From 479c9a3cca15451d2a9d66b9f14139597ba63b2c Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Wed, 24 Jan 2024 11:42:06 +0400 Subject: [PATCH 01/25] [ETH-769] Referral program banner view --- .../referral-icon.imageset/Contents.json | 22 ++++++++ .../referral-icon.imageset/image 2@2x.png | Bin 0 -> 29343 bytes .../referral-icon.imageset/image 2@3x.png | Bin 0 -> 52203 bytes .../share-3.imageset/Contents.json | 22 ++++++++ .../share-3.imageset/Share@2x.png | Bin 0 -> 683 bytes .../share-3.imageset/Share@3x.png | Bin 0 -> 809 bytes .../Resources/Base.lproj/Localizable.strings | 3 ++ .../Resources/en.lproj/Localizable.strings | 3 ++ .../Crypto Accounts/CryptoAccountsView.swift | 2 + .../Settings/View/SettingsView.swift | 9 ++++ .../Banner/ReferralProgramBannerView.swift | 49 ++++++++++++++++++ 11 files changed, 110 insertions(+) create mode 100644 p2p_wallet/Resources/Assets.xcassets/referral-icon.imageset/Contents.json create mode 100644 p2p_wallet/Resources/Assets.xcassets/referral-icon.imageset/image 2@2x.png create mode 100644 p2p_wallet/Resources/Assets.xcassets/referral-icon.imageset/image 2@3x.png create mode 100644 p2p_wallet/Resources/Assets.xcassets/share-3.imageset/Contents.json create mode 100644 p2p_wallet/Resources/Assets.xcassets/share-3.imageset/Share@2x.png create mode 100644 p2p_wallet/Resources/Assets.xcassets/share-3.imageset/Share@3x.png create mode 100644 p2p_wallet/Scenes/Main/ReferralProgram/Banner/ReferralProgramBannerView.swift diff --git a/p2p_wallet/Resources/Assets.xcassets/referral-icon.imageset/Contents.json b/p2p_wallet/Resources/Assets.xcassets/referral-icon.imageset/Contents.json new file mode 100644 index 0000000000..4946f2a4a2 --- /dev/null +++ b/p2p_wallet/Resources/Assets.xcassets/referral-icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "image 2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "image 2@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/p2p_wallet/Resources/Assets.xcassets/referral-icon.imageset/image 2@2x.png b/p2p_wallet/Resources/Assets.xcassets/referral-icon.imageset/image 2@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f759adbd3e422bdc87268154eedb51b0d04c9fbc GIT binary patch literal 29343 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 RR~7&O002ovPDHLkV1ja~F4+J8 literal 0 HcmV?d00001 diff --git a/p2p_wallet/Resources/Assets.xcassets/share-3.imageset/Share@3x.png b/p2p_wallet/Resources/Assets.xcassets/share-3.imageset/Share@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..958e8c4c4a42fbcf9c809854f871c326edbcf8cb GIT binary patch literal 809 zcmV+^1J?YBP)5!7(1b7o;gG@hGXOu{v+G5_>rG`76bvHn0E6&j0Re)^ z&7ap3*@RIM%nE0?aS9(m>CETDlwhMGkT;;lG5Z^ilY_=Y_?y8HH$|4wBRKF;a?F^B zEg{MR1eB8JsWC8w&G{NMV2uAtj@2$gWTczTV8H;sfCdD}GC5{LO+@Jwvk|7$F_eJz zcAt`Cm5Vr3@DZHPU2+{Hhbybtaq#$YoU>a1oLm9nXRxPUa1K`|J#Q?VD`MH21wZsU1MgFln7m8W)f+3LWA}bG?o+?dM2bU zmT__kdN_Dh7I+c-ADh7#YC>Ko{#yrOl^rNYEq5lTVnj0Yl ze?r4($~u5XMc7?i Void + var openDetails: () -> Void + + var body: some View { + VStack(spacing: 0) { + HStack(alignment: .top) { + Text(L10n.refferalProgramm) + .font(uiFont: .font(of: .title3, weight: .semibold)) + Spacer() + Image(uiImage: .init(resource: .referralIcon)) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 141) + .padding(.trailing, 12) + } + HStack(spacing: 8) { + NewTextButton( + title: L10n.shareMyLink, + size: .small, + style: .primary, + expandable: true, + trailing: UIImage(resource: .share3), + action: shareAction + ) + NewTextButton( + title: L10n.openDetails, + size: .small, + style: .inverted, + expandable: true, + action: openDetails + ) + } + } + .padding(.horizontal, 20) + .padding(.vertical, 16) + .background(Color(uiColor: UIColor(resource: .cdf6Cd))) + .cornerRadius(radius: 24, corners: .allCorners) + } +} + +#Preview { + VStack { + ReferralProgramBannerView(shareAction: {}, openDetails: {}) + .padding(.horizontal, 16) + } +} From 19b22c10e029fee9c9b5f9098158356432bb158a Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Wed, 24 Jan 2024 13:04:19 +0400 Subject: [PATCH 02/25] [ETH-772] Referral program feature toggle --- p2p_wallet/Common/Services/FeatureFlags/Features.swift | 3 +++ .../DebugMenu/ViewModel/DebugMenuViewModel.swift | 4 ++++ .../Crypto Accounts/CryptoAccountsView.swift | 10 ++++++++-- .../Crypto Accounts/CryptoAccountsViewModel.swift | 4 +++- .../Main/NewSettings/Settings/View/SettingsView.swift | 4 +++- .../Settings/ViewModel/SettingsViewModel.swift | 3 +++ 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/p2p_wallet/Common/Services/FeatureFlags/Features.swift b/p2p_wallet/Common/Services/FeatureFlags/Features.swift index 1b15432e67..205b66c64b 100755 --- a/p2p_wallet/Common/Services/FeatureFlags/Features.swift +++ b/p2p_wallet/Common/Services/FeatureFlags/Features.swift @@ -27,4 +27,7 @@ public extension Feature { static let sendViaLinkEnabled = Feature(rawValue: "send_via_link_enabled") static let solanaEthAddressEnabled = Feature(rawValue: "solana_eth_address_enabled") + + // Referral program + static let referralProgramEnabled = Feature(rawValue: "referral_program_enabled") } diff --git a/p2p_wallet/Scenes/DebugMenu/ViewModel/DebugMenuViewModel.swift b/p2p_wallet/Scenes/DebugMenu/ViewModel/DebugMenuViewModel.swift index e7916acdf7..bdbd730f83 100644 --- a/p2p_wallet/Scenes/DebugMenu/ViewModel/DebugMenuViewModel.swift +++ b/p2p_wallet/Scenes/DebugMenu/ViewModel/DebugMenuViewModel.swift @@ -112,6 +112,8 @@ extension DebugMenuViewModel { case onboardingUsernameEnabled case onboardingUsernameButtonSkipEnabled + case referralProgramEnabled + case investSolend case solendDisablePlaceholder @@ -139,6 +141,7 @@ extension DebugMenuViewModel { case .sendViaLink: return "Send via link" case .solanaEthAddressEnabled: return "solana ETH address enabled" case .swapTransactionSimulation: return "Swap transaction simulation" + case .referralProgramEnabled: return "Referral program enabled" } } @@ -157,6 +160,7 @@ extension DebugMenuViewModel { case .sendViaLink: return .sendViaLinkEnabled case .solanaEthAddressEnabled: return .solanaEthAddressEnabled case .swapTransactionSimulation: return .swapTransactionSimulationEnabled + case .referralProgramEnabled: return .referralProgramEnabled } } } diff --git a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift index a5d3d84c80..d5cadec1e8 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift @@ -34,8 +34,10 @@ struct CryptoAccountsView: View { .padding(.top, 5) .padding(.bottom, 32) .id(0) - ReferralProgramBannerView(shareAction: {}, openDetails: {}) - .padding(.horizontal, 16) + if viewModel.displayReferralBanner { + banner + .padding(.horizontal, 16) + } content } } @@ -60,6 +62,10 @@ struct CryptoAccountsView: View { actionsPanelView } + private var banner: some View { + ReferralProgramBannerView(shareAction: {}, openDetails: {}) + } + private var content: some View { VStack(alignment: .leading, spacing: 0) { if !viewModel.transferAccounts.isEmpty { diff --git a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift index 2ec0b64af9..d2724db72d 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift @@ -28,6 +28,8 @@ final class CryptoAccountsViewModel: BaseViewModel, ObservableObject { @Published private(set) var scrollOnTheTop = true @Published private(set) var hideZeroBalance: Bool = Defaults.hideZeroBalances + @Published private(set) var displayReferralBanner: Bool + /// Accounts for claiming transfers. @Published var transferAccounts: [any RenderableAccount] = [] @@ -53,7 +55,7 @@ final class CryptoAccountsViewModel: BaseViewModel, ObservableObject { self.userActionService = userActionService self.favouriteAccountsStore = favouriteAccountsStore self.navigation = navigation - + displayReferralBanner = available(.referralProgramEnabled) super.init() defaultsDisposables.append(Defaults.observe(\.hideZeroBalances) { [weak self] change in diff --git a/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift b/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift index 96f6962685..bc729390c4 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift @@ -18,7 +18,9 @@ struct SettingsView: View { List { Group { profileSection - referralProgramSection + if viewModel.isReferralProgramEnabled { + referralProgramSection + } securitySection appearanceSection communitySection diff --git a/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift b/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift index 1a6dc2be46..b64e139788 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift @@ -50,11 +50,14 @@ final class SettingsViewModel: BaseViewModel, ObservableObject { @Published var deviceShareMigrationAlert: Bool = false + @Published var isReferralProgramEnabled: Bool + var appInfo: String { AppInfo.appVersionDetail } override init() { + isReferralProgramEnabled = available(.referralProgramEnabled) super.init() setUpAuthType() updateNameIfNeeded() From 5cfa43d94346023f66cffecc087ee6ba9cedf74a Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Wed, 24 Jan 2024 16:29:49 +0400 Subject: [PATCH 03/25] [ETH-769] Add coordination for referral buttons banner --- .../Resources/Base.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 1 + .../Crypto Accounts/CryptoAccountsView.swift | 5 ++++- .../CryptoAccountsViewModel.swift | 21 +++++++++++++++++++ .../Crypto/Container/CryptoCoordinator.swift | 10 +++++++++ .../Settings/View/SettingsView.swift | 9 +++++--- .../ViewModel/SettingsViewModel.swift | 18 ++++++++++++++++ .../NewSettings/SettingsCoordinator.swift | 8 +++++++ .../Banner/ReferralProgramBannerView.swift | 4 ++++ .../Details/ReferralProgramCoordinator.swift | 21 +++++++++++++++++++ .../Details/ReferralProgramView.swift | 8 +++++++ 11 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift create mode 100644 p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift diff --git a/p2p_wallet/Resources/Base.lproj/Localizable.strings b/p2p_wallet/Resources/Base.lproj/Localizable.strings index e5efdc0fdb..a4d890a442 100644 --- a/p2p_wallet/Resources/Base.lproj/Localizable.strings +++ b/p2p_wallet/Resources/Base.lproj/Localizable.strings @@ -600,3 +600,4 @@ "Token 2022 transfer fee" = "Token 2022 transfer fee"; "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; +"Referral programm" = "Referral program"; diff --git a/p2p_wallet/Resources/en.lproj/Localizable.strings b/p2p_wallet/Resources/en.lproj/Localizable.strings index ab4b47b4cf..8c68df604d 100644 --- a/p2p_wallet/Resources/en.lproj/Localizable.strings +++ b/p2p_wallet/Resources/en.lproj/Localizable.strings @@ -588,3 +588,4 @@ "Token 2022 transfer fee" = "Token 2022 transfer fee"; "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; +"Referral programm" = "Referral program"; diff --git a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift index d5cadec1e8..54d9b88f9a 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift @@ -63,7 +63,10 @@ struct CryptoAccountsView: View { } private var banner: some View { - ReferralProgramBannerView(shareAction: {}, openDetails: {}) + ReferralProgramBannerView( + shareAction: viewModel.shareReferralLink.send, + openDetails: viewModel.openReferralProgramDetails.send + ) } private var content: some View { diff --git a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift index d2724db72d..f1a88c86bc 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift @@ -25,6 +25,9 @@ final class CryptoAccountsViewModel: BaseViewModel, ObservableObject { // MARK: - Properties + let openReferralProgramDetails = PassthroughSubject() + let shareReferralLink = PassthroughSubject() + @Published private(set) var scrollOnTheTop = true @Published private(set) var hideZeroBalance: Bool = Defaults.hideZeroBalances @@ -62,6 +65,8 @@ final class CryptoAccountsViewModel: BaseViewModel, ObservableObject { self?.hideZeroBalance = change.newValue ?? false }) + bind() + bindAccounts() } @@ -113,6 +118,22 @@ final class CryptoAccountsViewModel: BaseViewModel, ObservableObject { .store(in: &subscriptions) } + private func bind() { + openReferralProgramDetails + .map { CryptoNavigation.referral } + .sink { [weak self] navigation in + self?.navigation.send(navigation) + } + .store(in: &subscriptions) + + shareReferralLink + .map { CryptoNavigation.shareReferral(URL(string: "https://www.google.com/")!) } + .sink { [weak self] navigation in + self?.navigation.send(navigation) + } + .store(in: &subscriptions) + } + // MARK: - Actions func refresh() async { diff --git a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoCoordinator.swift b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoCoordinator.swift index d661384a84..fea95c8a6c 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoCoordinator.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoCoordinator.swift @@ -21,6 +21,8 @@ enum CryptoNavigation: Equatable { case solanaAccount(SolanaAccount) case claim(EthereumAccount, WormholeClaimUserAction?) case actions([WalletActionType]) + case referral + case shareReferral(URL) // Empty case topUpCoin(TokenMetadata) // Error @@ -203,6 +205,14 @@ final class CryptoCoordinator: Coordinator { }) .map { _ in () } .eraseToAnyPublisher() + case .referral: + return coordinate(to: ReferralProgramCoordinator(navigationController: navigationController)) + .eraseToAnyPublisher() + case let .shareReferral(link): + let activityVC = UIActivityViewController(activityItems: [link], applicationActivities: nil) + navigationController.present(activityVC, animated: true) + return Just(()) + .eraseToAnyPublisher() default: return Just(()) .eraseToAnyPublisher() diff --git a/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift b/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift index bc729390c4..fd5a298fb6 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift @@ -122,9 +122,12 @@ struct SettingsView: View { private var referralProgramSection: some View { Section { - ReferralProgramBannerView(shareAction: {}, openDetails: {}) - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + ReferralProgramBannerView( + shareAction: viewModel.shareReferralLink.send, + openDetails: viewModel.openReferralProgramDetails.send + ) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } } diff --git a/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift b/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift index b64e139788..09863ec980 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift @@ -51,6 +51,8 @@ final class SettingsViewModel: BaseViewModel, ObservableObject { @Published var deviceShareMigrationAlert: Bool = false @Published var isReferralProgramEnabled: Bool + let openReferralProgramDetails = PassthroughSubject() + let shareReferralLink = PassthroughSubject() var appInfo: String { AppInfo.appVersionDetail @@ -174,6 +176,20 @@ final class SettingsViewModel: BaseViewModel, ObservableObject { self.updateNameIfNeeded() } .store(in: &subscriptions) + + openReferralProgramDetails + .map { OpenAction.referral } + .sink { [weak self] navigation in + self?.openActionSubject.send(navigation) + } + .store(in: &subscriptions) + + shareReferralLink + .map { OpenAction.shareReferral(URL(string: "https://www.google.com/")!) } + .sink { [weak self] navigation in + self?.openActionSubject.send(navigation) + } + .store(in: &subscriptions) } func openTwitter() { @@ -197,5 +213,7 @@ extension SettingsViewModel { case reserveUsername(userAddress: String) case recoveryKit case yourPin + case referral + case shareReferral(URL) } } diff --git a/p2p_wallet/Scenes/Main/NewSettings/SettingsCoordinator.swift b/p2p_wallet/Scenes/Main/NewSettings/SettingsCoordinator.swift index 4be6504eec..308143ba31 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/SettingsCoordinator.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/SettingsCoordinator.swift @@ -40,6 +40,14 @@ final class SettingsCoordinator: Coordinator { navigationController.popToRootViewController(animated: true) }) .store(in: &subscriptions) + case .referral: + let coordinator = ReferralProgramCoordinator(navigationController: navigationController) + coordinate(to: coordinator) + .sink { _ in } + .store(in: &subscriptions) + case let .shareReferral(link): + let activityVC = UIActivityViewController(activityItems: [link], applicationActivities: nil) + navigationController.present(activityVC, animated: true) } }) .store(in: &subscriptions) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Banner/ReferralProgramBannerView.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Banner/ReferralProgramBannerView.swift index 1049742183..8026f6ebc3 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Banner/ReferralProgramBannerView.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Banner/ReferralProgramBannerView.swift @@ -17,6 +17,8 @@ struct ReferralProgramBannerView: View { .padding(.trailing, 12) } HStack(spacing: 8) { + // Must apply a button style for the correct behaviour inside a list + // https://stackoverflow.com/a/70400079 NewTextButton( title: L10n.shareMyLink, size: .small, @@ -25,6 +27,7 @@ struct ReferralProgramBannerView: View { trailing: UIImage(resource: .share3), action: shareAction ) + .buttonStyle(.plain) NewTextButton( title: L10n.openDetails, size: .small, @@ -32,6 +35,7 @@ struct ReferralProgramBannerView: View { expandable: true, action: openDetails ) + .buttonStyle(.plain) } } .padding(.horizontal, 20) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift new file mode 100644 index 0000000000..a1c4402fe1 --- /dev/null +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift @@ -0,0 +1,21 @@ +import Combine +import SwiftUI +import UIKit + +final class ReferralProgramCoordinator: Coordinator { + private let navigationController: UINavigationController + private let result = PassthroughSubject() + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + override func start() -> AnyPublisher { + let view = ReferralProgramView() + let vc = UIHostingController(rootView: view) + vc.hidesBottomBarWhenPushed = true + navigationController.pushViewController(vc, animated: true) + + return result.eraseToAnyPublisher() + } +} diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift new file mode 100644 index 0000000000..e9527ac771 --- /dev/null +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift @@ -0,0 +1,8 @@ +import SwiftUI + +struct ReferralProgramView: View { + var body: some View { + Text("Webview") + .navigationTitle(L10n.referralProgramm) + } +} From 066b59463f4046fd2bc2b918d49e90624f3cda46 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Thu, 25 Jan 2024 12:30:00 +0400 Subject: [PATCH 04/25] [ETH-769] Referral program banner on empty screen --- .../Crypto Accounts/CryptoAccountsView.swift | 14 ++++------- .../CryptoAccountsViewModel.swift | 24 ------------------- .../Crypto Empty/CryptoEmptyView.swift | 17 +++++++++---- .../Main/Crypto/Container/CryptoView.swift | 16 +++++++++++-- .../Crypto/Container/CryptoViewModel.swift | 19 +++++++++++++++ 5 files changed, 51 insertions(+), 39 deletions(-) diff --git a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift index 54d9b88f9a..2e2c1e9a75 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsView.swift @@ -9,6 +9,7 @@ struct CryptoAccountsView: View { @ObservedObject var viewModel: CryptoAccountsViewModel private let actionsPanelView: CryptoActionsPanelView + private let banner: ReferralProgramBannerView? @State var isHiddenSectionDisabled: Bool = true @State var currentUserInteractionCellID: String? @@ -18,10 +19,12 @@ struct CryptoAccountsView: View { init( viewModel: CryptoAccountsViewModel, - actionsPanelView: CryptoActionsPanelView + actionsPanelView: CryptoActionsPanelView, + banner: ReferralProgramBannerView? ) { self.viewModel = viewModel self.actionsPanelView = actionsPanelView + self.banner = banner } // MARK: - View content @@ -34,7 +37,7 @@ struct CryptoAccountsView: View { .padding(.top, 5) .padding(.bottom, 32) .id(0) - if viewModel.displayReferralBanner { + if let banner { banner .padding(.horizontal, 16) } @@ -62,13 +65,6 @@ struct CryptoAccountsView: View { actionsPanelView } - private var banner: some View { - ReferralProgramBannerView( - shareAction: viewModel.shareReferralLink.send, - openDetails: viewModel.openReferralProgramDetails.send - ) - } - private var content: some View { VStack(alignment: .leading, spacing: 0) { if !viewModel.transferAccounts.isEmpty { diff --git a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift index f1a88c86bc..68a7f1f940 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Accounts/CryptoAccountsViewModel.swift @@ -25,14 +25,9 @@ final class CryptoAccountsViewModel: BaseViewModel, ObservableObject { // MARK: - Properties - let openReferralProgramDetails = PassthroughSubject() - let shareReferralLink = PassthroughSubject() - @Published private(set) var scrollOnTheTop = true @Published private(set) var hideZeroBalance: Bool = Defaults.hideZeroBalances - @Published private(set) var displayReferralBanner: Bool - /// Accounts for claiming transfers. @Published var transferAccounts: [any RenderableAccount] = [] @@ -58,15 +53,12 @@ final class CryptoAccountsViewModel: BaseViewModel, ObservableObject { self.userActionService = userActionService self.favouriteAccountsStore = favouriteAccountsStore self.navigation = navigation - displayReferralBanner = available(.referralProgramEnabled) super.init() defaultsDisposables.append(Defaults.observe(\.hideZeroBalances) { [weak self] change in self?.hideZeroBalance = change.newValue ?? false }) - bind() - bindAccounts() } @@ -118,22 +110,6 @@ final class CryptoAccountsViewModel: BaseViewModel, ObservableObject { .store(in: &subscriptions) } - private func bind() { - openReferralProgramDetails - .map { CryptoNavigation.referral } - .sink { [weak self] navigation in - self?.navigation.send(navigation) - } - .store(in: &subscriptions) - - shareReferralLink - .map { CryptoNavigation.shareReferral(URL(string: "https://www.google.com/")!) } - .sink { [weak self] navigation in - self?.navigation.send(navigation) - } - .store(in: &subscriptions) - } - // MARK: - Actions func refresh() async { diff --git a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Empty/CryptoEmptyView.swift b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Empty/CryptoEmptyView.swift index 3b8b6d9e7b..aaa00ca25b 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Empty/CryptoEmptyView.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Components/Crypto Empty/CryptoEmptyView.swift @@ -6,21 +6,30 @@ struct CryptoEmptyView: View { // MARK: - Properties private let actionsPanelView: CryptoActionsPanelView + private let banner: ReferralProgramBannerView? // MARK: - Initializer init( - actionsPanelView: CryptoActionsPanelView + actionsPanelView: CryptoActionsPanelView, + banner: ReferralProgramBannerView? ) { self.actionsPanelView = actionsPanelView + self.banner = banner } // MARK: - View content var body: some View { - VStack(spacing: 30) { - header - content + ScrollView { + VStack(spacing: 30) { + header + if let banner { + banner + .padding(.horizontal, 16) + } + content + } } } diff --git a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoView.swift b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoView.swift index b935a32969..2552ed6f11 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoView.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoView.swift @@ -28,6 +28,16 @@ struct CryptoView: View { CryptoActionsPanelView(viewModel: actionsPanelViewModel) } + private var banner: ReferralProgramBannerView? { + if viewModel.displayReferralBanner { + return ReferralProgramBannerView( + shareAction: viewModel.shareReferralLink.send, + openDetails: viewModel.openReferralProgramDetails.send + ) + } + return nil + } + var body: some View { ZStack { Color(.smoke) @@ -37,12 +47,14 @@ struct CryptoView: View { CryptoPendingView() case .empty: CryptoEmptyView( - actionsPanelView: actionsPanelView + actionsPanelView: actionsPanelView, + banner: banner ) case .accounts: CryptoAccountsView( viewModel: accountsViewModel, - actionsPanelView: actionsPanelView + actionsPanelView: actionsPanelView, + banner: banner ) } } diff --git a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift index 1af470d5ed..229c273fa2 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift @@ -25,9 +25,12 @@ final class CryptoViewModel: BaseViewModel, ObservableObject { @Injected private var createNameService: CreateNameService let navigation: PassthroughSubject + let openReferralProgramDetails = PassthroughSubject() + let shareReferralLink = PassthroughSubject() // MARK: - Properties + @Published private(set) var displayReferralBanner: Bool @Published var state = State.pending @Published var address = "" @@ -35,6 +38,8 @@ final class CryptoViewModel: BaseViewModel, ObservableObject { init(navigation: PassthroughSubject) { self.navigation = navigation + displayReferralBanner = available(.referralProgramEnabled) + super.init() // bind @@ -175,6 +180,20 @@ private extension CryptoViewModel { self?.updateAddressIfNeeded() } .store(in: &subscriptions) + + openReferralProgramDetails + .map { CryptoNavigation.referral } + .sink { [weak self] navigation in + self?.navigation.send(navigation) + } + .store(in: &subscriptions) + + shareReferralLink + .map { CryptoNavigation.shareReferral(URL(string: "https://www.google.com/")!) } + .sink { [weak self] navigation in + self?.navigation.send(navigation) + } + .store(in: &subscriptions) } } From bcb22e14b7fdd0497eaea27be3faa4b76e682fe2 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Thu, 25 Jan 2024 16:29:39 +0400 Subject: [PATCH 05/25] [ETH-769] Mock share cell --- .../Resources/Base.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 1 + .../Settings/View/SettingsView.swift | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/p2p_wallet/Resources/Base.lproj/Localizable.strings b/p2p_wallet/Resources/Base.lproj/Localizable.strings index a4d890a442..b2db33741f 100644 --- a/p2p_wallet/Resources/Base.lproj/Localizable.strings +++ b/p2p_wallet/Resources/Base.lproj/Localizable.strings @@ -601,3 +601,4 @@ "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; "Referral programm" = "Referral program"; +"My referral link" = "My referral link"; diff --git a/p2p_wallet/Resources/en.lproj/Localizable.strings b/p2p_wallet/Resources/en.lproj/Localizable.strings index 8c68df604d..52c7647978 100644 --- a/p2p_wallet/Resources/en.lproj/Localizable.strings +++ b/p2p_wallet/Resources/en.lproj/Localizable.strings @@ -589,3 +589,4 @@ "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; "Referral programm" = "Referral program"; +"My referral link" = "My referral link"; diff --git a/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift b/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift index fd5a298fb6..345e165e18 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift @@ -128,6 +128,31 @@ struct SettingsView: View { ) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(L10n.myReferralLink) + .font(uiFont: .font(of: .text3, weight: .semibold)) + Text("mock/@team") + .apply(style: .label1) + .foregroundColor(Color(uiColor: UIColor(resource: .mountain))) + } + Spacer() + NewTextButton( + title: L10n.share, + size: .small, + style: .primary, + trailing: UIImage(resource: .share3), + action: viewModel.shareReferralLink.send + ) + .buttonStyle(.plain) + } + .padding(.all, 16) + .background(Color.white) + .cornerRadius(16) + .listRowInsets(EdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0)) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) } } From b8dcfb6cdc5007a864899193b255f2a2219e39db Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Fri, 26 Jan 2024 17:36:42 +0400 Subject: [PATCH 06/25] [ETH-778] Referral program webview and localizes fixes --- .../Resources/Base.lproj/Localizable.strings | 3 +- .../Resources/en.lproj/Localizable.strings | 3 +- .../Crypto/Container/CryptoCoordinator.swift | 5 ++- .../Details/ReferralProgramCoordinator.swift | 2 +- .../Details/ReferralProgramView.swift | 31 +++++++++++++++++-- .../Details/ReferralProgramViewModel.swift | 15 +++++++++ 6 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift diff --git a/p2p_wallet/Resources/Base.lproj/Localizable.strings b/p2p_wallet/Resources/Base.lproj/Localizable.strings index b2db33741f..4bb88466e1 100644 --- a/p2p_wallet/Resources/Base.lproj/Localizable.strings +++ b/p2p_wallet/Resources/Base.lproj/Localizable.strings @@ -600,5 +600,6 @@ "Token 2022 transfer fee" = "Token 2022 transfer fee"; "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; -"Referral programm" = "Referral program"; +"Referral program" = "Referral program"; "My referral link" = "My referral link"; +"Hey, let’s swap trendy meme coins with me!" = "Hey, let’s swap trendy meme coins with me!"; diff --git a/p2p_wallet/Resources/en.lproj/Localizable.strings b/p2p_wallet/Resources/en.lproj/Localizable.strings index 52c7647978..e5f9cd5237 100644 --- a/p2p_wallet/Resources/en.lproj/Localizable.strings +++ b/p2p_wallet/Resources/en.lproj/Localizable.strings @@ -588,5 +588,6 @@ "Token 2022 transfer fee" = "Token 2022 transfer fee"; "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; -"Referral programm" = "Referral program"; +"Referral program" = "Referral program"; "My referral link" = "My referral link"; +"Hey, let’s swap trendy meme coins with me!" = "Hey, let’s swap trendy meme coins with me!"; diff --git a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoCoordinator.swift b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoCoordinator.swift index fea95c8a6c..ba18875744 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoCoordinator.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoCoordinator.swift @@ -209,7 +209,10 @@ final class CryptoCoordinator: Coordinator { return coordinate(to: ReferralProgramCoordinator(navigationController: navigationController)) .eraseToAnyPublisher() case let .shareReferral(link): - let activityVC = UIActivityViewController(activityItems: [link], applicationActivities: nil) + let activityVC = UIActivityViewController( + activityItems: ["\(L10n.heyLetSSwapTrendyMemeCoinsWithMe) \(link)"], + applicationActivities: nil + ) navigationController.present(activityVC, animated: true) return Just(()) .eraseToAnyPublisher() diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift index a1c4402fe1..096bbc4583 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift @@ -11,7 +11,7 @@ final class ReferralProgramCoordinator: Coordinator { } override func start() -> AnyPublisher { - let view = ReferralProgramView() + let view = ReferralProgramView(viewModel: ReferralProgramViewModel()) let vc = UIHostingController(rootView: view) vc.hidesBottomBarWhenPushed = true navigationController.pushViewController(vc, animated: true) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift index e9527ac771..a766f3e454 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift @@ -1,8 +1,35 @@ import SwiftUI +import WebKit struct ReferralProgramView: View { + @ObservedObject private var viewModel: ReferralProgramViewModel + + init(viewModel: ReferralProgramViewModel) { + self.viewModel = viewModel + } + var body: some View { - Text("Webview") - .navigationTitle(L10n.referralProgramm) + ColoredBackground( + { + WebView(url: viewModel.link) + .ignoresSafeArea(edges: .bottom) + }, + color: Color(uiColor: UIColor(resource: .f2F5Fa)) + ) + .navigationTitle(L10n.referralProgram) + .navigationBarTitleDisplayMode(.inline) + } +} + +struct WebView: UIViewRepresentable { + let url: URL + + func makeUIView(context _: Context) -> WKWebView { + WKWebView() + } + + func updateUIView(_ webView: WKWebView, context _: Context) { + let request = URLRequest(url: url) + webView.load(request) } } diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift new file mode 100644 index 0000000000..c4becce216 --- /dev/null +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift @@ -0,0 +1,15 @@ +import Combine +import Foundation + +private enum Constants { + static let urlString = "https://referral-2ii.pages.dev" +} + +final class ReferralProgramViewModel: BaseViewModel, ObservableObject { + let link: URL + + override init() { + link = URL(string: Constants.urlString)! + super.init() + } +} From 44d43a1d76749b94ea94586cec969d824aa4d3c2 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Mon, 29 Jan 2024 14:46:06 +0400 Subject: [PATCH 07/25] [ETH-778] Do ui fixes for referral banner --- .../Resources/Base.lproj/Localizable.strings | 2 -- .../Resources/en.lproj/Localizable.strings | 2 -- .../Settings/View/SettingsView.swift | 25 ------------------- .../Banner/ReferralProgramBannerView.swift | 2 +- 4 files changed, 1 insertion(+), 30 deletions(-) diff --git a/p2p_wallet/Resources/Base.lproj/Localizable.strings b/p2p_wallet/Resources/Base.lproj/Localizable.strings index 4bb88466e1..76d32e869b 100644 --- a/p2p_wallet/Resources/Base.lproj/Localizable.strings +++ b/p2p_wallet/Resources/Base.lproj/Localizable.strings @@ -593,7 +593,6 @@ "The token %@ from your swap link seems suspicious, therefore we've refreshed swap pair to default." = "The token %@ from your swap link seems suspicious, therefore we've refreshed swap pair to default."; "Charge that you need to pay to send or receive tokenA. It helps maintain the network and ensure smooth transactions." = "Charge that you need to pay to send or receive tokenA. It helps maintain the network and ensure smooth transactions."; "Unfortunately, you can not cashout in %@, but you can still use other Key App features" = "Unfortunately, you can not cashout in %@, but you can still use other Key App features"; -"Refferal\nprogramm" = "Refferal\nprogramm"; "Share my link" = "Share my link"; "Open details" = "Open details"; "Token 2022 details" = "Token 2022 details"; @@ -601,5 +600,4 @@ "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; "Referral program" = "Referral program"; -"My referral link" = "My referral link"; "Hey, let’s swap trendy meme coins with me!" = "Hey, let’s swap trendy meme coins with me!"; diff --git a/p2p_wallet/Resources/en.lproj/Localizable.strings b/p2p_wallet/Resources/en.lproj/Localizable.strings index e5f9cd5237..d18f0f62ec 100644 --- a/p2p_wallet/Resources/en.lproj/Localizable.strings +++ b/p2p_wallet/Resources/en.lproj/Localizable.strings @@ -581,7 +581,6 @@ "The token %@ is out of the strict list" = "The token %@ is out of the strict list"; "Make sure the mint address %@ is correct before confirming" = "Make sure the mint address %@ is correct before confirming"; "Unfortunately, you can not cashout in %@, but you can still use other Key App features" = "Unfortunately, you can not cashout in %@, but you can still use other Key App features"; -"Refferal\nprogramm" = "Refferal\nprogramm"; "Share my link" = "Share my link"; "Open details" = "Open details"; "Token 2022 details" = "Token 2022 details"; @@ -589,5 +588,4 @@ "Calculated by subtracting the token 2022 transfer fee from your balance" = "Calculated by subtracting the token 2022 transfer fee from your balance"; "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance" = "Calculated by subtracting the token 2022 transfer fee and account creation fee from your balance"; "Referral program" = "Referral program"; -"My referral link" = "My referral link"; "Hey, let’s swap trendy meme coins with me!" = "Hey, let’s swap trendy meme coins with me!"; diff --git a/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift b/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift index 345e165e18..fd5a298fb6 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/Settings/View/SettingsView.swift @@ -128,31 +128,6 @@ struct SettingsView: View { ) .listRowBackground(Color.clear) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(L10n.myReferralLink) - .font(uiFont: .font(of: .text3, weight: .semibold)) - Text("mock/@team") - .apply(style: .label1) - .foregroundColor(Color(uiColor: UIColor(resource: .mountain))) - } - Spacer() - NewTextButton( - title: L10n.share, - size: .small, - style: .primary, - trailing: UIImage(resource: .share3), - action: viewModel.shareReferralLink.send - ) - .buttonStyle(.plain) - } - .padding(.all, 16) - .background(Color.white) - .cornerRadius(16) - .listRowInsets(EdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0)) - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) } } diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Banner/ReferralProgramBannerView.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Banner/ReferralProgramBannerView.swift index 8026f6ebc3..514590d3cd 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Banner/ReferralProgramBannerView.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Banner/ReferralProgramBannerView.swift @@ -7,7 +7,7 @@ struct ReferralProgramBannerView: View { var body: some View { VStack(spacing: 0) { HStack(alignment: .top) { - Text(L10n.refferalProgramm) + Text(L10n.referralProgram.replacingOccurrences(of: " ", with: "\n")) .font(uiFont: .font(of: .title3, weight: .semibold)) Spacer() Image(uiImage: .init(resource: .referralIcon)) From 64d527cfe7520da91dd49072f8627c76e6da1c0f Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Tue, 30 Jan 2024 14:03:33 +0400 Subject: [PATCH 08/25] [ETH-778] WIP ReferralBridge and debug menu --- p2p_wallet/Common/Services/Defaults.swift | 1 + .../Common/Services/GlobalAppState.swift | 13 ++ .../Scenes/DebugMenu/View/DebugMenuView.swift | 7 + .../ReferralProgram/Details/ReferralBridge.js | 35 +++++ .../Details/ReferralJSBridge.swift | 129 ++++++++++++++++++ .../Details/ReferralProgramCoordinator.swift | 14 +- .../Details/ReferralProgramView.swift | 21 +-- .../Details/ReferralProgramViewModel.swift | 40 +++++- .../Details/ReferralWebView.swift | 21 +++ 9 files changed, 259 insertions(+), 22 deletions(-) create mode 100644 p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js create mode 100644 p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift create mode 100644 p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralWebView.swift diff --git a/p2p_wallet/Common/Services/Defaults.swift b/p2p_wallet/Common/Services/Defaults.swift index 183918f41e..849558ed4d 100644 --- a/p2p_wallet/Common/Services/Defaults.swift +++ b/p2p_wallet/Common/Services/Defaults.swift @@ -41,6 +41,7 @@ extension DefaultsKeys { var forcedFeeRelayerEndpoint: DefaultsKey { .init(#function, defaultValue: nil) } var forcedNameServiceEndpoint: DefaultsKey { .init(#function, defaultValue: nil) } var forcedNewSwapEndpoint: DefaultsKey { .init(#function, defaultValue: nil) } + var forcedReferralProgramEndpoint: DefaultsKey { .init(#function, defaultValue: nil) } var didBackupOffline: DefaultsKey { .init(#function, defaultValue: false) } var walletName: DefaultsKey<[String: String]> { .init(#function, defaultValue: [:]) } diff --git a/p2p_wallet/Common/Services/GlobalAppState.swift b/p2p_wallet/Common/Services/GlobalAppState.swift index a766d276df..b1de0aed8e 100644 --- a/p2p_wallet/Common/Services/GlobalAppState.swift +++ b/p2p_wallet/Common/Services/GlobalAppState.swift @@ -38,6 +38,13 @@ class GlobalAppState: ObservableObject { } } + @Published var newReferralProgramEndpoint: String { + didSet { + Defaults.forcedReferralProgramEndpoint = newReferralProgramEndpoint + ResolverScope.session.reset() + } + } + // TODO: Refactor! @Published var surveyID: String? @Published var sendViaLinkUrl: URL? @@ -55,6 +62,12 @@ class GlobalAppState: ObservableObject { } else { newSwapEndpoint = "https://swap-v6.key.app" } + + if let forcedValue = Defaults.forcedReferralProgramEndpoint { + newReferralProgramEndpoint = forcedValue + } else { + newReferralProgramEndpoint = ReferralProgramViewModel.Constants.urlString + } } @Published var bridgeEndpoint: String = (Environment.current == .release) ? diff --git a/p2p_wallet/Scenes/DebugMenu/View/DebugMenuView.swift b/p2p_wallet/Scenes/DebugMenu/View/DebugMenuView.swift index b21574904b..669f3beaa7 100644 --- a/p2p_wallet/Scenes/DebugMenu/View/DebugMenuView.swift +++ b/p2p_wallet/Scenes/DebugMenu/View/DebugMenuView.swift @@ -21,6 +21,7 @@ struct DebugMenuView: View { solanaEndpoint swapEndpoint nameServiceEndpoint + referralEndpoint } featureTogglers @@ -145,4 +146,10 @@ struct DebugMenuView: View { TextField("New swap endpoint", text: $globalAppState.newSwapEndpoint) } } + + var referralEndpoint: some View { + Section(header: Text("New referral program endpoint")) { + TextField("Value", text: $globalAppState.newReferralProgramEndpoint) + } + } } diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js new file mode 100644 index 0000000000..3c66660c1c --- /dev/null +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js @@ -0,0 +1,35 @@ +const handleRequest = (args) => { + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.request) { + return window.webkit.messageHandlers.request.postMessage(args).then((result) => { + console.log(result); + if (result.error) { + console.log(result.error); + return Promise.reject(result.error); + } + + if (result === "null") { + return Promise.resolve(); + } + return result; + }); + } + return { code: 4900, message: "Host is not ready" } +}; + +class ReferralBridge { + static nativeLog(info) { + handleRequest({ method: "nativeLog", info: info }); + } + + static showShareDialog(link) { + handleRequest({ method: "showShareDialog", link: link }); + } + + static signTransactionAsync() { + handleRequest({ method: "signTransaction", link: link }); + } + + static getUserPublicKey() { + handleRequest({ method: "getUserPublicKey", link: link }); + } +} diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift new file mode 100644 index 0000000000..672474841a --- /dev/null +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift @@ -0,0 +1,129 @@ +import Combine +import Foundation +import Network +import Resolver +import WebKit + +private enum ReferralJSBridgeMethod: String { + case showShareDialog + case nativeLog + case signTransaction +} + +protocol ReferralBridge { + var sharePublisher: AnyPublisher { get } +} + +final class ReferralJSBridge: NSObject, ReferralBridge { + var sharePublisher: AnyPublisher { shareSubject.eraseToAnyPublisher() } + + // MARK: - Dependencies + + private var logger = DefaultLogManager.shared + @Injected private var userWalletManager: UserWalletManager + @Injected private var nameStorage: NameStorageType + + // MARK: - Properties + + private let shareSubject = PassthroughSubject() + + private var subscriptions: [AnyCancellable] = [] + private weak var webView: WKWebView? + + private var address: String? + private var domainName: String? + + public init(webView: WKWebView) { + self.webView = webView + super.init() + + userWalletManager.$wallet + .map { $0?.account.publicKey.base58EncodedString } + .assignWeak(to: \.address, on: self) + .store(in: &subscriptions) + + domainName = nameStorage.getName() + } + + func reload() { + Task { + await MainActor.run { webView?.reload() } + } + } + + func loadScript(name: String) -> String? { + guard let path = Bundle.main.path(forResource: name, ofType: "js") else { return nil } + do { + return try String(contentsOfFile: path) + } catch { + return nil + } + } + + public func inject() { + guard let bridgeScript = loadScript(name: "ReferralBridge") else { + debugPrint("Inject provider failure") + return + } + + guard let contentController = webView?.configuration.userContentController else { return } + contentController.addScriptMessageHandler(self, contentWorld: .page, name: "request") + + let script = WKUserScript(source: bridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: false) + contentController.addUserScript(script) + } +} + +extension ReferralJSBridge: WKScriptMessageHandlerWithReply { + public func userContentController( + _: WKUserContentController, + didReceive message: WKScriptMessage, + replyHandler: @escaping (Any?, String?) -> Void + ) { + guard let dict = message.body as? [String: AnyObject] else { + replyHandler(true, "Error") + return + } + + guard let methodRaw = dict["method"] as? String, + let method = ReferralJSBridgeMethod(rawValue: methodRaw) + else { + replyHandler(true, nil) + return + } + + // Overload reply handler + let replyHandler: (Any?, String?) -> Void = { [weak self] result, _ in + guard let self else { return } + if let error = (result as? [String: Any])?["error"] { + self.logger.log(event: "ReferralProgramLog", data: String(describing: error), logLevel: LogLevel.error) + } else { + self.logger.log(event: "ReferralProgramLog", data: String(describing: result), logLevel: LogLevel.info) + } + + replyHandler(result, nil) + } + + switch method { + case .showShareDialog: + if let link = dict["link"] as? String { + shareSubject.send(link) + replyHandler(link, nil) + } else { + replyHandler(nil, "Empty link") + } + + case .nativeLog: + if let info = dict["info"] as? String { + replyHandler(info, nil) + } else { + replyHandler(nil, "Empty info") + } + + case .signTransaction: + Task { + replyHandler(true, nil) + } + } + } +} diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift index 096bbc4583..c09fb04c74 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift @@ -11,10 +11,22 @@ final class ReferralProgramCoordinator: Coordinator { } override func start() -> AnyPublisher { - let view = ReferralProgramView(viewModel: ReferralProgramViewModel()) + let viewModel = ReferralProgramViewModel() + let view = ReferralProgramView(viewModel: viewModel) let vc = UIHostingController(rootView: view) vc.hidesBottomBarWhenPushed = true navigationController.pushViewController(vc, animated: true) + + viewModel.openShare + .sink { [weak vc] link in + let activityVC = UIActivityViewController( + activityItems: ["\(L10n.heyLetSSwapTrendyMemeCoinsWithMe) \(link)"], + applicationActivities: nil + ) + vc?.present(activityVC, animated: true) + } + .store(in: &subscriptions) + return result.eraseToAnyPublisher() } diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift index a766f3e454..32481ed5f8 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramView.swift @@ -1,5 +1,4 @@ import SwiftUI -import WebKit struct ReferralProgramView: View { @ObservedObject private var viewModel: ReferralProgramViewModel @@ -11,8 +10,11 @@ struct ReferralProgramView: View { var body: some View { ColoredBackground( { - WebView(url: viewModel.link) - .ignoresSafeArea(edges: .bottom) + ReferralWebView( + webView: viewModel.webView, + link: viewModel.link + ) + .ignoresSafeArea(edges: .bottom) }, color: Color(uiColor: UIColor(resource: .f2F5Fa)) ) @@ -20,16 +22,3 @@ struct ReferralProgramView: View { .navigationBarTitleDisplayMode(.inline) } } - -struct WebView: UIViewRepresentable { - let url: URL - - func makeUIView(context _: Context) -> WKWebView { - WKWebView() - } - - func updateUIView(_ webView: WKWebView, context _: Context) { - let request = URLRequest(url: url) - webView.load(request) - } -} diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift index c4becce216..9d8f6d9c0c 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift @@ -1,15 +1,45 @@ import Combine import Foundation - -private enum Constants { - static let urlString = "https://referral-2ii.pages.dev" -} +import WebKit final class ReferralProgramViewModel: BaseViewModel, ObservableObject { + enum Constants { + static let urlString = "https://referral-2ii.pages.dev" + } + let link: URL + let bridge: ReferralJSBridge + let webView: WKWebView + + let openShare = PassthroughSubject() override init() { - link = URL(string: Constants.urlString)! + let wkWebView = ReferralProgramViewModel.buildWebView() + webView = wkWebView + bridge = ReferralJSBridge(webView: wkWebView) + link = URL(string: GlobalAppState.shared.newReferralProgramEndpoint) ?? URL(string: Constants.urlString)! super.init() + + bridge.inject() + + bridge.sharePublisher + .sink(receiveValue: { [weak self] value in + self?.openShare.send(value) + }) + .store(in: &subscriptions) + } + + private static func buildWebView() -> WKWebView { + let userContentController = WKUserContentController() + let configuration = WKWebViewConfiguration() + configuration.userContentController = userContentController + let preferences = WKPreferences() + configuration.preferences = preferences + let webView = WKWebView(frame: .zero, configuration: configuration) + + if #available(iOS 16.4, *) { + webView.isInspectable = true + } + return webView } } diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralWebView.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralWebView.swift new file mode 100644 index 0000000000..9cf1d242c4 --- /dev/null +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralWebView.swift @@ -0,0 +1,21 @@ +import SwiftUI +import WebKit + +struct ReferralWebView: UIViewRepresentable { + private let url: URL + private let wkWebView: WKWebView + + init(webView: WKWebView, link: URL) { + wkWebView = webView + url = link + } + + func makeUIView(context _: Context) -> WKWebView { + wkWebView + } + + func updateUIView(_: WKWebView, context _: Context) { + let request = URLRequest(url: url) + wkWebView.load(request) + } +} From a8bcce1de7f8fd7c841e36f4d322eaad461f9da2 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Tue, 30 Jan 2024 14:11:00 +0400 Subject: [PATCH 09/25] [ETH-778] Add endpoint to config --- p2p_wallet/Common/Services/GlobalAppState.swift | 2 +- p2p_wallet/Info.plist | 2 ++ .../ReferralProgram/Details/ReferralProgramViewModel.swift | 7 ++----- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/p2p_wallet/Common/Services/GlobalAppState.swift b/p2p_wallet/Common/Services/GlobalAppState.swift index b1de0aed8e..6439c8250e 100644 --- a/p2p_wallet/Common/Services/GlobalAppState.swift +++ b/p2p_wallet/Common/Services/GlobalAppState.swift @@ -66,7 +66,7 @@ class GlobalAppState: ObservableObject { if let forcedValue = Defaults.forcedReferralProgramEndpoint { newReferralProgramEndpoint = forcedValue } else { - newReferralProgramEndpoint = ReferralProgramViewModel.Constants.urlString + newReferralProgramEndpoint = String.secretConfig("REFERRAL_PROGRAM_ENDPOINT")! } } diff --git a/p2p_wallet/Info.plist b/p2p_wallet/Info.plist index 17d104cedb..d4c3b76a4f 100644 --- a/p2p_wallet/Info.plist +++ b/p2p_wallet/Info.plist @@ -2,6 +2,8 @@ + REFERRAL_PROGRAM_ENDPOINT + $(REFERRAL_PROGRAM_ENDPOINT) AMPLITUDE_API_KEY $(AMPLITUDE_API_KEY) AMPLITUDE_API_KEY_FEATURE diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift index 9d8f6d9c0c..0a06ad353b 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift @@ -3,10 +3,6 @@ import Foundation import WebKit final class ReferralProgramViewModel: BaseViewModel, ObservableObject { - enum Constants { - static let urlString = "https://referral-2ii.pages.dev" - } - let link: URL let bridge: ReferralJSBridge let webView: WKWebView @@ -17,7 +13,8 @@ final class ReferralProgramViewModel: BaseViewModel, ObservableObject { let wkWebView = ReferralProgramViewModel.buildWebView() webView = wkWebView bridge = ReferralJSBridge(webView: wkWebView) - link = URL(string: GlobalAppState.shared.newReferralProgramEndpoint) ?? URL(string: Constants.urlString)! + link = URL(string: GlobalAppState.shared.newReferralProgramEndpoint) ?? + URL(string: String.secretConfig("REFERRAL_PROGRAM_ENDPOINT")!)! super.init() bridge.inject() From b513c9eb47bd8fad0e76bee5c44b5513cf26318c Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Tue, 30 Jan 2024 14:15:32 +0400 Subject: [PATCH 10/25] Revert "[ETH-778] Add endpoint to config" This reverts commit a8bcce1de7f8fd7c841e36f4d322eaad461f9da2. --- p2p_wallet/Common/Services/GlobalAppState.swift | 2 +- p2p_wallet/Info.plist | 2 -- .../ReferralProgram/Details/ReferralProgramViewModel.swift | 7 +++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/p2p_wallet/Common/Services/GlobalAppState.swift b/p2p_wallet/Common/Services/GlobalAppState.swift index 6439c8250e..b1de0aed8e 100644 --- a/p2p_wallet/Common/Services/GlobalAppState.swift +++ b/p2p_wallet/Common/Services/GlobalAppState.swift @@ -66,7 +66,7 @@ class GlobalAppState: ObservableObject { if let forcedValue = Defaults.forcedReferralProgramEndpoint { newReferralProgramEndpoint = forcedValue } else { - newReferralProgramEndpoint = String.secretConfig("REFERRAL_PROGRAM_ENDPOINT")! + newReferralProgramEndpoint = ReferralProgramViewModel.Constants.urlString } } diff --git a/p2p_wallet/Info.plist b/p2p_wallet/Info.plist index d4c3b76a4f..17d104cedb 100644 --- a/p2p_wallet/Info.plist +++ b/p2p_wallet/Info.plist @@ -2,8 +2,6 @@ - REFERRAL_PROGRAM_ENDPOINT - $(REFERRAL_PROGRAM_ENDPOINT) AMPLITUDE_API_KEY $(AMPLITUDE_API_KEY) AMPLITUDE_API_KEY_FEATURE diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift index 0a06ad353b..9d8f6d9c0c 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramViewModel.swift @@ -3,6 +3,10 @@ import Foundation import WebKit final class ReferralProgramViewModel: BaseViewModel, ObservableObject { + enum Constants { + static let urlString = "https://referral-2ii.pages.dev" + } + let link: URL let bridge: ReferralJSBridge let webView: WKWebView @@ -13,8 +17,7 @@ final class ReferralProgramViewModel: BaseViewModel, ObservableObject { let wkWebView = ReferralProgramViewModel.buildWebView() webView = wkWebView bridge = ReferralJSBridge(webView: wkWebView) - link = URL(string: GlobalAppState.shared.newReferralProgramEndpoint) ?? - URL(string: String.secretConfig("REFERRAL_PROGRAM_ENDPOINT")!)! + link = URL(string: GlobalAppState.shared.newReferralProgramEndpoint) ?? URL(string: Constants.urlString)! super.init() bridge.inject() From 317dbfb54e0930a86777df88586952eb7bcfd660 Mon Sep 17 00:00:00 2001 From: runner Date: Tue, 30 Jan 2024 10:16:41 +0000 Subject: [PATCH 11/25] fix(swiftformat): Apply Swiftformat changes --- .../ReferralProgram/Details/ReferralProgramCoordinator.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift index c09fb04c74..6b3cc89431 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralProgramCoordinator.swift @@ -16,7 +16,7 @@ final class ReferralProgramCoordinator: Coordinator { let vc = UIHostingController(rootView: view) vc.hidesBottomBarWhenPushed = true navigationController.pushViewController(vc, animated: true) - + viewModel.openShare .sink { [weak vc] link in let activityVC = UIActivityViewController( @@ -26,7 +26,6 @@ final class ReferralProgramCoordinator: Coordinator { vc?.present(activityVC, animated: true) } .store(in: &subscriptions) - return result.eraseToAnyPublisher() } From e53521919ab24b66672e045f3fe28b854d4fb2ab Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Tue, 30 Jan 2024 16:03:42 +0400 Subject: [PATCH 12/25] [ETH-778] Add user public key method handler --- .../ReferralProgram/Details/ReferralBridge.js | 28 +++++++++---------- .../Details/ReferralJSBridge.swift | 28 +++++++++++++------ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js index 3c66660c1c..16148152ae 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js @@ -1,4 +1,4 @@ -const handleRequest = (args) => { +const handleRequest = async (args) => { if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.request) { return window.webkit.messageHandlers.request.postMessage(args).then((result) => { console.log(result); @@ -16,20 +16,20 @@ const handleRequest = (args) => { return { code: 4900, message: "Host is not ready" } }; -class ReferralBridge { - static nativeLog(info) { +window.ReferralBridge = { + getUserPublicKey: async function() { + const result = await handleRequest({ method: "getUserPublicKey" }); + ReferralBridge.nativeLog(result); + return result + }, + nativeLog: function(info) { handleRequest({ method: "nativeLog", info: info }); - } - - static showShareDialog(link) { + }, + showShareDialog: function(link) { handleRequest({ method: "showShareDialog", link: link }); - } - - static signTransactionAsync() { - handleRequest({ method: "signTransaction", link: link }); - } - - static getUserPublicKey() { - handleRequest({ method: "getUserPublicKey", link: link }); + }, + signMessageAsync: async function(message) { + const result = await handleRequest({ method: "signTransaction", message: message }); + return result } } diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift index 672474841a..5dc54fa1e7 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift @@ -8,6 +8,7 @@ private enum ReferralJSBridgeMethod: String { case showShareDialog case nativeLog case signTransaction + case getUserPublicKey } protocol ReferralBridge { @@ -93,9 +94,9 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { } // Overload reply handler - let replyHandler: (Any?, String?) -> Void = { [weak self] result, _ in + let handler: (Any?, String?) -> Void = { [weak self] result, error in guard let self else { return } - if let error = (result as? [String: Any])?["error"] { + if let error { self.logger.log(event: "ReferralProgramLog", data: String(describing: error), logLevel: LogLevel.error) } else { self.logger.log(event: "ReferralProgramLog", data: String(describing: result), logLevel: LogLevel.info) @@ -108,21 +109,32 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { case .showShareDialog: if let link = dict["link"] as? String { shareSubject.send(link) - replyHandler(link, nil) + handler(link, nil) } else { - replyHandler(nil, "Empty link") + handler(nil, "Empty link") } case .nativeLog: if let info = dict["info"] as? String { - replyHandler(info, nil) + debugPrint(info) + handler(info, nil) } else { - replyHandler(nil, "Empty info") + handler(nil, "Empty info") } case .signTransaction: - Task { - replyHandler(true, nil) + if let message = dict["message"] as? String { + Task { + // TODO: https://linear.app/etherean/issue/ETH-806/[ios]-podpis-zaprosov-privatnym-klyuchom + handler(message, nil) + } + } + + case .getUserPublicKey: + if let address { + handler(address, nil) + } else { + handler(nil, "Empty address") } } } From 47e7118e6ea74bc6add35cc07da8f1444f5a5666 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Tue, 30 Jan 2024 21:13:28 +0400 Subject: [PATCH 13/25] [ETH-778] Handle result as an object in js bridge --- .../Main/ReferralProgram/Details/ReferralBridge.js | 2 +- .../ReferralProgram/Details/ReferralJSBridge.swift | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js index 16148152ae..4562cf082e 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js @@ -10,7 +10,7 @@ const handleRequest = async (args) => { if (result === "null") { return Promise.resolve(); } - return result; + return {value: result}; }); } return { code: 4900, message: "Host is not ready" } diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift index 5dc54fa1e7..07606c2453 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift @@ -94,15 +94,15 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { } // Overload reply handler - let handler: (Any?, String?) -> Void = { [weak self] result, error in + let handler: (String?, String?) -> Void = { [weak self] result, error in guard let self else { return } - if let error { - self.logger.log(event: "ReferralProgramLog", data: String(describing: error), logLevel: LogLevel.error) - } else { + if let result { self.logger.log(event: "ReferralProgramLog", data: String(describing: result), logLevel: LogLevel.info) + replyHandler(result, nil) + } else { + self.logger.log(event: "ReferralProgramLog", data: String(describing: error), logLevel: LogLevel.error) + replyHandler(nil, error) } - - replyHandler(result, nil) } switch method { @@ -128,6 +128,8 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { // TODO: https://linear.app/etherean/issue/ETH-806/[ios]-podpis-zaprosov-privatnym-klyuchom handler(message, nil) } + } else { + handler(nil, "Empty message") } case .getUserPublicKey: From 29a8ed23e051ac2f59947fc898c4f1e85d520454 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Wed, 31 Jan 2024 11:46:18 +0400 Subject: [PATCH 14/25] [ETH-806] Add signature method and fix some models --- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Details/Bridge/BridgeModels.swift | 13 ++++ .../Details/{ => Bridge}/ReferralBridge.js | 2 +- .../{ => Bridge}/ReferralJSBridge.swift | 66 +++++++++---------- 4 files changed, 47 insertions(+), 38 deletions(-) create mode 100644 p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/BridgeModels.swift rename p2p_wallet/Scenes/Main/ReferralProgram/Details/{ => Bridge}/ReferralBridge.js (92%) rename p2p_wallet/Scenes/Main/ReferralProgram/Details/{ => Bridge}/ReferralJSBridge.swift (68%) diff --git a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c84e1b1f95..d5f64b8eba 100644 --- a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -266,8 +266,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ashleymills/Reachability.swift.git", "state" : { - "revision" : "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2", - "version" : "5.1.0" + "revision" : "c01127cb51f591045696128effe43c16840d08bf", + "version" : "5.2.0" } }, { diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/BridgeModels.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/BridgeModels.swift new file mode 100644 index 0000000000..1a1745538d --- /dev/null +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/BridgeModels.swift @@ -0,0 +1,13 @@ +enum ReferralBridgeMethod: String { + case showShareDialog + case nativeLog + case signMessage + case getUserPublicKey +} + +enum ReferralBridgeError: String { + case emptyAddress + case emptyLog + case signFailed + case emptyLink +} diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralBridge.js similarity index 92% rename from p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js rename to p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralBridge.js index 4562cf082e..334f161d56 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralBridge.js +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralBridge.js @@ -29,7 +29,7 @@ window.ReferralBridge = { handleRequest({ method: "showShareDialog", link: link }); }, signMessageAsync: async function(message) { - const result = await handleRequest({ method: "signTransaction", message: message }); + const result = await handleRequest({ method: "signMessage", message: message }); return result } } diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift similarity index 68% rename from p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift rename to p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift index 07606c2453..57e0a46aca 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/ReferralJSBridge.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift @@ -2,15 +2,10 @@ import Combine import Foundation import Network import Resolver +import SolanaSwift +import TweetNacl import WebKit -private enum ReferralJSBridgeMethod: String { - case showShareDialog - case nativeLog - case signTransaction - case getUserPublicKey -} - protocol ReferralBridge { var sharePublisher: AnyPublisher { get } } @@ -22,28 +17,16 @@ final class ReferralJSBridge: NSObject, ReferralBridge { private var logger = DefaultLogManager.shared @Injected private var userWalletManager: UserWalletManager - @Injected private var nameStorage: NameStorageType // MARK: - Properties private let shareSubject = PassthroughSubject() - private var subscriptions: [AnyCancellable] = [] private weak var webView: WKWebView? - private var address: String? - private var domainName: String? - public init(webView: WKWebView) { self.webView = webView super.init() - - userWalletManager.$wallet - .map { $0?.account.publicKey.base58EncodedString } - .assignWeak(to: \.address, on: self) - .store(in: &subscriptions) - - domainName = nameStorage.getName() } func reload() { @@ -87,31 +70,40 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { } guard let methodRaw = dict["method"] as? String, - let method = ReferralJSBridgeMethod(rawValue: methodRaw) + let method = ReferralBridgeMethod(rawValue: methodRaw) else { replyHandler(true, nil) return } // Overload reply handler - let handler: (String?, String?) -> Void = { [weak self] result, error in + let handler: (String?, ReferralBridgeError?) -> Void = { [weak self] result, error in guard let self else { return } if let result { self.logger.log(event: "ReferralProgramLog", data: String(describing: result), logLevel: LogLevel.info) replyHandler(result, nil) } else { - self.logger.log(event: "ReferralProgramLog", data: String(describing: error), logLevel: LogLevel.error) - replyHandler(nil, error) + self.logger.log( + event: "ReferralProgramLog", + data: String(describing: error?.rawValue), + logLevel: LogLevel.error + ) + replyHandler(nil, error?.rawValue) } } + guard let user = userWalletManager.wallet else { + handler(nil, .emptyAddress) + return + } + switch method { case .showShareDialog: if let link = dict["link"] as? String { shareSubject.send(link) handler(link, nil) } else { - handler(nil, "Empty link") + handler(nil, .emptyLink) } case .nativeLog: @@ -119,25 +111,29 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { debugPrint(info) handler(info, nil) } else { - handler(nil, "Empty info") + handler(nil, .emptyLog) } - case .signTransaction: - if let message = dict["message"] as? String { + case .signMessage: + if let message = dict["message"] as? String, + let user = userWalletManager.wallet, + let base64Data = Data(base64Encoded: message, options: .ignoreUnknownCharacters) + { Task { - // TODO: https://linear.app/etherean/issue/ETH-806/[ios]-podpis-zaprosov-privatnym-klyuchom - handler(message, nil) + do { + let signed = try NaclSign.signDetached(message: base64Data, secretKey: user.account.secretKey) + let signatureBase58 = Base58.encode(signed) + handler(signatureBase58, nil) + } catch { + handler(nil, .signFailed) + } } } else { - handler(nil, "Empty message") + handler(nil, .signFailed) } case .getUserPublicKey: - if let address { - handler(address, nil) - } else { - handler(nil, "Empty address") - } + handler(user.account.publicKey.base58EncodedString, nil) } } } From a70ebafa1e392a3ee248a4c59bbc729b3b5b0bf3 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Wed, 31 Jan 2024 13:23:28 +0400 Subject: [PATCH 15/25] [ETH-769] Link to referral --- .../ReferralProgramService.swift | 18 ++++++++++++++++++ .../Resolver+registerAllServices.swift | 3 +++ .../Crypto/Container/CryptoViewModel.swift | 6 +++++- .../Settings/ViewModel/SettingsViewModel.swift | 6 +++++- .../Main/NewSettings/SettingsCoordinator.swift | 5 ++++- 5 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift diff --git a/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift new file mode 100644 index 0000000000..75767c97e8 --- /dev/null +++ b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift @@ -0,0 +1,18 @@ +import Foundation +import KeyAppBusiness +import Resolver + +protocol ReferralProgramService { + var link: URL { get } +} + +final class ReferralProgramServiceImpl: ReferralProgramService { + var link: URL { + let referral = nameStorage.getName() ?? solanaAccountsService.state.value.nativeWallet? + .address ?? "" + return URL(string: "https://r.key.app/\(referral)")! + } + + @Injected private var nameStorage: NameStorageType + @Injected private var solanaAccountsService: SolanaAccountsService +} diff --git a/p2p_wallet/Injection/Resolver+registerAllServices.swift b/p2p_wallet/Injection/Resolver+registerAllServices.swift index c628ff748e..66b6eeb760 100644 --- a/p2p_wallet/Injection/Resolver+registerAllServices.swift +++ b/p2p_wallet/Injection/Resolver+registerAllServices.swift @@ -289,6 +289,9 @@ extension Resolver: ResolverRegistering { .scope(.application) register { Web3(rpcURL: String.secretConfig("ETH_RPC")!) } + + register { ReferralProgramServiceImpl() } + .implements(ReferralProgramService.self) } /// Session scope: Live when user is authenticated diff --git a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift index 229c273fa2..396e885c2a 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift @@ -23,6 +23,7 @@ final class CryptoViewModel: BaseViewModel, ObservableObject { @Injected private var nameStorage: NameStorageType @Injected private var sellDataService: any SellDataService @Injected private var createNameService: CreateNameService + @Injected private var referralService: ReferralProgramService let navigation: PassthroughSubject let openReferralProgramDetails = PassthroughSubject() @@ -189,7 +190,10 @@ private extension CryptoViewModel { .store(in: &subscriptions) shareReferralLink - .map { CryptoNavigation.shareReferral(URL(string: "https://www.google.com/")!) } + .compactMap { [weak self] in + guard let self else { return nil } + return CryptoNavigation.shareReferral(self.referralService.link) + } .sink { [weak self] navigation in self?.navigation.send(navigation) } diff --git a/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift b/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift index 09863ec980..3647adaa8b 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift @@ -16,6 +16,7 @@ final class SettingsViewModel: BaseViewModel, ObservableObject { @Injected private var metadataService: WalletMetadataService @Injected private var createNameService: CreateNameService @Injected private var deviceShareMigrationService: DeviceShareMigrationService + @Injected private var referralService: ReferralProgramService @Published var zeroBalancesIsHidden = Defaults.hideZeroBalances { didSet { @@ -185,7 +186,10 @@ final class SettingsViewModel: BaseViewModel, ObservableObject { .store(in: &subscriptions) shareReferralLink - .map { OpenAction.shareReferral(URL(string: "https://www.google.com/")!) } + .compactMap { [weak self] in + guard let self else { return nil } + return OpenAction.shareReferral(self.referralService.link) + } .sink { [weak self] navigation in self?.openActionSubject.send(navigation) } diff --git a/p2p_wallet/Scenes/Main/NewSettings/SettingsCoordinator.swift b/p2p_wallet/Scenes/Main/NewSettings/SettingsCoordinator.swift index 308143ba31..13790030fe 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/SettingsCoordinator.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/SettingsCoordinator.swift @@ -46,7 +46,10 @@ final class SettingsCoordinator: Coordinator { .sink { _ in } .store(in: &subscriptions) case let .shareReferral(link): - let activityVC = UIActivityViewController(activityItems: [link], applicationActivities: nil) + let activityVC = UIActivityViewController( + activityItems: ["\(L10n.heyLetSSwapTrendyMemeCoinsWithMe) \(link)"], + applicationActivities: nil + ) navigationController.present(activityVC, animated: true) } }) From 4b94da123fd648eac54bc35c7c8d10602515df68 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Wed, 31 Jan 2024 13:42:41 +0400 Subject: [PATCH 16/25] [ETH-778] Rename bridge method for public key --- .../Main/ReferralProgram/Details/Bridge/ReferralBridge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralBridge.js b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralBridge.js index 334f161d56..3b64088dae 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralBridge.js +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralBridge.js @@ -17,7 +17,7 @@ const handleRequest = async (args) => { }; window.ReferralBridge = { - getUserPublicKey: async function() { + getUserPublicKeyAsync: async function() { const result = await handleRequest({ method: "getUserPublicKey" }); ReferralBridge.nativeLog(result); return result From 91a44d5df714ea8486694e2cd30158f9817e57f0 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Wed, 31 Jan 2024 14:28:07 +0400 Subject: [PATCH 17/25] [ETH-778] Return base64 instead of base58 for signed message --- .../ReferralProgram/Details/Bridge/ReferralJSBridge.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift index 57e0a46aca..ccfe4b6f6a 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift @@ -121,9 +121,8 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { { Task { do { - let signed = try NaclSign.signDetached(message: base64Data, secretKey: user.account.secretKey) - let signatureBase58 = Base58.encode(signed) - handler(signatureBase58, nil) + let signed = try NaclSign.signDetached(message: base64Data, secretKey: user.account.secretKey).base64EncodedString() + handler(signed, nil) } catch { handler(nil, .signFailed) } From aef00a3fa3c0eeef3a981a94e27f99773ce2b866 Mon Sep 17 00:00:00 2001 From: runner Date: Wed, 31 Jan 2024 10:29:01 +0000 Subject: [PATCH 18/25] fix(swiftformat): Apply Swiftformat changes --- .../Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift index ccfe4b6f6a..ac546138c1 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift @@ -121,7 +121,8 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { { Task { do { - let signed = try NaclSign.signDetached(message: base64Data, secretKey: user.account.secretKey).base64EncodedString() + let signed = try NaclSign.signDetached(message: base64Data, secretKey: user.account.secretKey) + .base64EncodedString() handler(signed, nil) } catch { handler(nil, .signFailed) From 164016a5c235701d83a11ae0561c1ff90465c929 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Thu, 1 Feb 2024 13:58:09 +0400 Subject: [PATCH 19/25] [PWN-831] WIP Referral program service --- .../DeeplinkAppDelegateService.swift | 25 +++++- .../Common/Services/GlobalAppState.swift | 2 + .../ReferralProgramService.swift | 88 +++++++++++++++++-- p2p_wallet/Info.plist | 2 + .../Crypto/Container/CryptoViewModel.swift | 2 +- .../ViewModel/SettingsViewModel.swift | 2 +- .../Details/Bridge/ReferralJSBridge.swift | 2 +- .../Swap/Swap/JupiterSwapCoordinator.swift | 3 +- .../Scenes/Main/Swap/Swap/SwapViewModel.swift | 1 + 9 files changed, 114 insertions(+), 13 deletions(-) diff --git a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift index f6fa47d197..9189773fad 100644 --- a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift +++ b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift @@ -68,9 +68,17 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService { } // Swap via link - // https://s.key.app/swap?inputMint=&outputMint= + // https://s.key.app/swap?inputMint=&outputMint=&r= if urlComponents.host == "s.key.app" { GlobalAppState.shared.swapUrl = urlComponents.url + + if let referrer = urlComponents.queryItems?.first { $0.name == "r" }?.value { + setReferrer(r: referrer) + } + } + + if urlComponents.host == "r.key.app" { + debugPrint(urlComponents) } } @@ -122,6 +130,21 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService { GlobalAppState.shared.swapUrl = components.url } } + + private func setReferrer(r: String) { + let referralService: ReferralProgramService = Resolver.resolve() + Task { + do { + _ = try await referralService.setReferent(from: r) + } catch { + DefaultLogManager.shared.log( + event: "ReferralProgram.set_referrent", + data: error.localizedDescription, + logLevel: LogLevel.error + ) + } + } + } } // MARK: - AppFlyer's DeepLinkDelegate diff --git a/p2p_wallet/Common/Services/GlobalAppState.swift b/p2p_wallet/Common/Services/GlobalAppState.swift index b1de0aed8e..f88279a0ef 100644 --- a/p2p_wallet/Common/Services/GlobalAppState.swift +++ b/p2p_wallet/Common/Services/GlobalAppState.swift @@ -45,6 +45,8 @@ class GlobalAppState: ObservableObject { } } + @Published var referralProgramAPIEndoint = String.secretConfig("REFERRAL_PROGRAM_API_ENDPOINT_PROD")! + // TODO: Refactor! @Published var surveyID: String? @Published var sendViaLinkUrl: URL? diff --git a/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift index 75767c97e8..27872b6f41 100644 --- a/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift +++ b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift @@ -1,18 +1,90 @@ import Foundation import KeyAppBusiness +import KeyAppNetworking import Resolver +import TweetNacl protocol ReferralProgramService { - var link: URL { get } + var referrer: String { get } + var shareLink: URL { get } + + func setReferent(from: String) async throws -> String } -final class ReferralProgramServiceImpl: ReferralProgramService { - var link: URL { - let referral = nameStorage.getName() ?? solanaAccountsService.state.value.nativeWallet? - .address ?? "" - return URL(string: "https://r.key.app/\(referral)")! - } +enum ReferralProgramServiceError: Error { + case failedSet +} + +final class ReferralProgramServiceImpl { + // MARK: - Dependencies @Injected private var nameStorage: NameStorageType - @Injected private var solanaAccountsService: SolanaAccountsService + @Injected private var userWallet: UserWalletManager + + private let jsonrpcClient = JSONRPCHTTPClient() + + // MARK: - Private properties + + private var baseURL: String { + GlobalAppState.shared.referralProgramAPIEndoint + } + + private var currentUserAddress: String { + userWallet.wallet?.account.publicKey.base58EncodedString ?? "" + } +} + +// MARK: - ReferralProgramService + +extension ReferralProgramServiceImpl: ReferralProgramService { + var referrer: String { + nameStorage.getName() ?? currentUserAddress + } + + var shareLink: URL { + URL(string: "https://r.key.app/\(referrer)")! + } + + func setReferent(from: String) async throws -> String { + guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet } + var timestamp = Date().timeIntervalSince1970 + let model = ReferralSetReferentModel(user: currentUserAddress, referent: referrer, timestamp: timestamp) + let data = try JSONEncoder().encode(model) + let signed = try NaclSign.signDetached(message: data, secretKey: secret) + return try await jsonrpcClient.request( + baseURL: baseURL, + body: .init( + method: "set_referent", + params: ReferralSetReferentRequest( + user: currentUserAddress, + referent: from, + timedSignature: .init( + timestamp: timestamp, + signature: signed.base64EncodedString() + ) + ) + ) + ) + } +} + +private struct ReferralSetReferentRequest: Encodable { + struct TimedSignature: Encodable { + let timestamp: TimeInterval + let signature: String + } + + let user: String + let referent: String + let timedSignature: TimedSignature + + enum CodingKeys: String, CodingKey { + case user, referent, timedSignature = "timed_signature" + } +} + +private struct ReferralSetReferentModel: Encodable { + let user: String + let referent: String + let timestamp: TimeInterval } diff --git a/p2p_wallet/Info.plist b/p2p_wallet/Info.plist index 17d104cedb..422744fec3 100644 --- a/p2p_wallet/Info.plist +++ b/p2p_wallet/Info.plist @@ -2,6 +2,8 @@ + REFERRAL_PROGRAM_API_ENDPOINT_PROD + $(REFERRAL_PROGRAM_API_ENDPOINT_PROD) AMPLITUDE_API_KEY $(AMPLITUDE_API_KEY) AMPLITUDE_API_KEY_FEATURE diff --git a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift index 396e885c2a..05f610f69b 100644 --- a/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift +++ b/p2p_wallet/Scenes/Main/Crypto/Container/CryptoViewModel.swift @@ -192,7 +192,7 @@ private extension CryptoViewModel { shareReferralLink .compactMap { [weak self] in guard let self else { return nil } - return CryptoNavigation.shareReferral(self.referralService.link) + return CryptoNavigation.shareReferral(self.referralService.shareLink) } .sink { [weak self] navigation in self?.navigation.send(navigation) diff --git a/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift b/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift index 3647adaa8b..8b7452aaf4 100644 --- a/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift +++ b/p2p_wallet/Scenes/Main/NewSettings/Settings/ViewModel/SettingsViewModel.swift @@ -188,7 +188,7 @@ final class SettingsViewModel: BaseViewModel, ObservableObject { shareReferralLink .compactMap { [weak self] in guard let self else { return nil } - return OpenAction.shareReferral(self.referralService.link) + return OpenAction.shareReferral(self.referralService.shareLink) } .sink { [weak self] navigation in self?.openActionSubject.send(navigation) diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift index ac546138c1..53f46cc99f 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift @@ -15,7 +15,7 @@ final class ReferralJSBridge: NSObject, ReferralBridge { // MARK: - Dependencies - private var logger = DefaultLogManager.shared + private let logger = DefaultLogManager.shared @Injected private var userWalletManager: UserWalletManager // MARK: - Properties diff --git a/p2p_wallet/Scenes/Main/Swap/Swap/JupiterSwapCoordinator.swift b/p2p_wallet/Scenes/Main/Swap/Swap/JupiterSwapCoordinator.swift index 11a36aeff3..68d2b16f21 100644 --- a/p2p_wallet/Scenes/Main/Swap/Swap/JupiterSwapCoordinator.swift +++ b/p2p_wallet/Scenes/Main/Swap/Swap/JupiterSwapCoordinator.swift @@ -189,8 +189,9 @@ final class JupiterSwapCoordinator: Coordinator { let from = viewModel.currentState.fromToken.mintAddress let to = viewModel.currentState.toToken.mintAddress + let referrer = viewModel.referralProgramService.referrer - let items = ["https://s.key.app/swap?from=\(from)&to=\(to)"] + let items = ["https://s.key.app/swap?from=\(from)&to=\(to)&r=\(referrer)"] let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil) navigationController.present(activityVC, animated: true) diff --git a/p2p_wallet/Scenes/Main/Swap/Swap/SwapViewModel.swift b/p2p_wallet/Scenes/Main/Swap/Swap/SwapViewModel.swift index ccae471afb..6d49693cbe 100644 --- a/p2p_wallet/Scenes/Main/Swap/Swap/SwapViewModel.swift +++ b/p2p_wallet/Scenes/Main/Swap/Swap/SwapViewModel.swift @@ -18,6 +18,7 @@ final class SwapViewModel: BaseViewModel, ObservableObject { // MARK: - Dependencies + @Injected private(set) var referralProgramService: ReferralProgramService @Injected private var swapWalletsRepository: JupiterTokensRepository @Injected private var notificationService: NotificationService @Injected private var transactionHandler: TransactionHandler From 0d223f19cb4fb0dd1a2435dc9e15bbc222ec0de1 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Thu, 1 Feb 2024 14:56:03 +0400 Subject: [PATCH 20/25] [ETH-831] Signature fix for referrer set --- .../DeeplinkAppDelegateService.swift | 5 +- .../ReferralProgramService.swift | 72 +++++++++++-------- .../Details/Bridge/ReferralJSBridge.swift | 22 +++++- .../Swap/Swap/JupiterSwapCoordinator.swift | 8 ++- 4 files changed, 72 insertions(+), 35 deletions(-) diff --git a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift index 9189773fad..5eda09d211 100644 --- a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift +++ b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift @@ -73,7 +73,7 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService { GlobalAppState.shared.swapUrl = urlComponents.url if let referrer = urlComponents.queryItems?.first { $0.name == "r" }?.value { - setReferrer(r: referrer) + setReferrerIfNeeded(r: referrer) } } @@ -131,7 +131,8 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService { } } - private func setReferrer(r: String) { + private func setReferrerIfNeeded(r: String) { + guard available(.referralProgramEnabled) else { return } let referralService: ReferralProgramService = Resolver.resolve() Task { do { diff --git a/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift index 27872b6f41..e5263957ca 100644 --- a/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift +++ b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift @@ -2,6 +2,7 @@ import Foundation import KeyAppBusiness import KeyAppNetworking import Resolver +import SolanaSwift import TweetNacl protocol ReferralProgramService { @@ -37,6 +38,43 @@ final class ReferralProgramServiceImpl { // MARK: - ReferralProgramService extension ReferralProgramServiceImpl: ReferralProgramService { + private struct SetReferentRequest: Encodable { + struct TimedSignature: Encodable { + let timestamp: Int64 + let signature: String + } + + let user: String + let referent: String + let timedSignature: TimedSignature + + enum CodingKeys: String, CodingKey { + case user, referent, timedSignature = "timed_signature" + } + } + + private struct SetReferentSignature: BorshSerializable { + let user: String + let referent: String + let timestamp: Int64 + + func serialize(to writer: inout Data) throws { + try user.serialize(to: &writer) + try referent.serialize(to: &writer) + try timestamp.serialize(to: &writer) + } + + func sign(secretKey: Data) throws -> Data { + var data = Data() + try serialize(to: &data) + return try NaclSign.signDetached(message: data, secretKey: secretKey) + } + + func signAsBase64(secretKey: Data) throws -> String { + try sign(secretKey: secretKey).base64EncodedString() + } + } + var referrer: String { nameStorage.getName() ?? currentUserAddress } @@ -47,44 +85,22 @@ extension ReferralProgramServiceImpl: ReferralProgramService { func setReferent(from: String) async throws -> String { guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet } - var timestamp = Date().timeIntervalSince1970 - let model = ReferralSetReferentModel(user: currentUserAddress, referent: referrer, timestamp: timestamp) - let data = try JSONEncoder().encode(model) - let signed = try NaclSign.signDetached(message: data, secretKey: secret) + var timestamp = Int64(Date().timeIntervalSince1970) + let signed = try SetReferentSignature(user: currentUserAddress, referent: referrer, timestamp: timestamp) + .signAsBase64(secretKey: secret) return try await jsonrpcClient.request( baseURL: baseURL, body: .init( method: "set_referent", - params: ReferralSetReferentRequest( + params: SetReferentRequest( user: currentUserAddress, referent: from, - timedSignature: .init( + timedSignature: SetReferentRequest.TimedSignature( timestamp: timestamp, - signature: signed.base64EncodedString() + signature: signed ) ) ) ) } } - -private struct ReferralSetReferentRequest: Encodable { - struct TimedSignature: Encodable { - let timestamp: TimeInterval - let signature: String - } - - let user: String - let referent: String - let timedSignature: TimedSignature - - enum CodingKeys: String, CodingKey { - case user, referent, timedSignature = "timed_signature" - } -} - -private struct ReferralSetReferentModel: Encodable { - let user: String - let referent: String - let timestamp: TimeInterval -} diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift index 53f46cc99f..5e9fed5ab6 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift @@ -121,8 +121,8 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { { Task { do { - let signed = try NaclSign.signDetached(message: base64Data, secretKey: user.account.secretKey) - .base64EncodedString() + let signed = try SignMessageSignature(message: message) + .signAsBase64(secretKey: user.account.secretKey) handler(signed, nil) } catch { handler(nil, .signFailed) @@ -137,3 +137,21 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { } } } + +struct SignMessageSignature: Codable, BorshSerializable { + let message: String + + func serialize(to writer: inout Data) throws { + try message.serialize(to: &writer) + } + + func sign(secretKey: Data) throws -> Data { + var data = Data() + try serialize(to: &data) + return try NaclSign.signDetached(message: data, secretKey: secretKey) + } + + func signAsBase64(secretKey: Data) throws -> String { + try sign(secretKey: secretKey).base64EncodedString() + } +} diff --git a/p2p_wallet/Scenes/Main/Swap/Swap/JupiterSwapCoordinator.swift b/p2p_wallet/Scenes/Main/Swap/Swap/JupiterSwapCoordinator.swift index 68d2b16f21..81f05a9bf9 100644 --- a/p2p_wallet/Scenes/Main/Swap/Swap/JupiterSwapCoordinator.swift +++ b/p2p_wallet/Scenes/Main/Swap/Swap/JupiterSwapCoordinator.swift @@ -189,10 +189,12 @@ final class JupiterSwapCoordinator: Coordinator { let from = viewModel.currentState.fromToken.mintAddress let to = viewModel.currentState.toToken.mintAddress - let referrer = viewModel.referralProgramService.referrer + var item = "https://s.key.app/swap?from=\(from)&to=\(to)" + if available(.referralProgramEnabled) { + item.append("&r=\(viewModel.referralProgramService.referrer)") + } - let items = ["https://s.key.app/swap?from=\(from)&to=\(to)&r=\(referrer)"] - let activityVC = UIActivityViewController(activityItems: items, applicationActivities: nil) + let activityVC = UIActivityViewController(activityItems: [item], applicationActivities: nil) navigationController.present(activityVC, animated: true) } From b9e8092a111e675aaa3be672feedc1793c35c8c4 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Thu, 1 Feb 2024 17:13:56 +0400 Subject: [PATCH 21/25] [ETH-831] Register referrent --- Packages/KeyAppKit/Package.swift | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../AppCoordinator/AppCoordinator.swift | 6 + .../DeeplinkAppDelegateService.swift | 18 ++- p2p_wallet/Common/Services/Defaults.swift | 4 + .../Model/ReferralTimedSignature.swift | 6 + .../Model/RegisterUserRequest.swift | 30 +++++ .../Model/SetReferentRequest.swift | 31 +++++ .../ReferralProgramService.swift | 116 ++++++++++-------- .../Details/Bridge/ReferralJSBridge.swift | 23 +--- p2p_wallet/p2p_wallet.entitlements | 1 + 11 files changed, 152 insertions(+), 89 deletions(-) create mode 100644 p2p_wallet/Common/Services/ReferralProgram/Model/ReferralTimedSignature.swift create mode 100644 p2p_wallet/Common/Services/ReferralProgram/Model/RegisterUserRequest.swift create mode 100644 p2p_wallet/Common/Services/ReferralProgram/Model/SetReferentRequest.swift diff --git a/Packages/KeyAppKit/Package.swift b/Packages/KeyAppKit/Package.swift index c8d5234503..e9aba20202 100644 --- a/Packages/KeyAppKit/Package.swift +++ b/Packages/KeyAppKit/Package.swift @@ -128,7 +128,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/p2p-org/solana-swift", branch: "main"), + .package(url: "https://github.com/p2p-org/solana-swift", branch: "feature/pwn-783"), .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.6.0")), .package(url: "https://github.com/Boilertalk/Web3.swift.git", from: "0.6.0"), // .package(url: "https://github.com/trustwallet/wallet-core", branch: "master"), diff --git a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1c24089c5c..ac4071a4fd 100644 --- a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -311,8 +311,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/p2p-org/solana-swift", "state" : { - "branch" : "main", - "revision" : "f9181079014c474c0e823a2f3f615ba953b41f1e" + "branch" : "feature/pwn-783", + "revision" : "ca0b6b46dab4975a336a848e9e38d75adc3ebab0" } }, { diff --git a/p2p_wallet/AppDelegate/AppCoordinator/AppCoordinator.swift b/p2p_wallet/AppDelegate/AppCoordinator/AppCoordinator.swift index c854a28527..795f333db5 100644 --- a/p2p_wallet/AppDelegate/AppCoordinator/AppCoordinator.swift +++ b/p2p_wallet/AppDelegate/AppCoordinator/AppCoordinator.swift @@ -131,6 +131,12 @@ final class AppCoordinator: Coordinator { await Resolver.resolve(JupiterTokensRepository.self).load() } + if available(.referralProgramEnabled) { + Task { + await Resolver.resolve(ReferralProgramService.self).register() + } + } + Task { // load services if available(.sellScenarioEnabled) { diff --git a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift index 5eda09d211..94aae150a8 100644 --- a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift +++ b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift @@ -78,7 +78,7 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService { } if urlComponents.host == "r.key.app" { - debugPrint(urlComponents) + setReferrerIfNeeded(r: urlComponents.path) } } @@ -129,21 +129,17 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService { else if scheme == "keyapp", host == "swap" { GlobalAppState.shared.swapUrl = components.url } + // keyapp://referral + else if scheme == "keyapp", host == "referral" { + setReferrerIfNeeded(r: components.path) + } } private func setReferrerIfNeeded(r: String) { - guard available(.referralProgramEnabled) else { return } +// guard available(.referralProgramEnabled) else { return } let referralService: ReferralProgramService = Resolver.resolve() Task { - do { - _ = try await referralService.setReferent(from: r) - } catch { - DefaultLogManager.shared.log( - event: "ReferralProgram.set_referrent", - data: error.localizedDescription, - logLevel: LogLevel.error - ) - } + _ = await referralService.setReferent(from: r) } } } diff --git a/p2p_wallet/Common/Services/Defaults.swift b/p2p_wallet/Common/Services/Defaults.swift index 849558ed4d..ae83f98b45 100644 --- a/p2p_wallet/Common/Services/Defaults.swift +++ b/p2p_wallet/Common/Services/Defaults.swift @@ -124,6 +124,10 @@ extension DefaultsKeys { var ethBannerShouldHide: DefaultsKey { .init(#function, defaultValue: false) } + + var referrerRegistered: DefaultsKey { + .init(#function, defaultValue: false) + } } // MARK: - Moonpay Environment diff --git a/p2p_wallet/Common/Services/ReferralProgram/Model/ReferralTimedSignature.swift b/p2p_wallet/Common/Services/ReferralProgram/Model/ReferralTimedSignature.swift new file mode 100644 index 0000000000..b4ee0f0430 --- /dev/null +++ b/p2p_wallet/Common/Services/ReferralProgram/Model/ReferralTimedSignature.swift @@ -0,0 +1,6 @@ +import Foundation + +struct ReferralTimedSignature: Encodable { + let timestamp: Int64 + let signature: String +} diff --git a/p2p_wallet/Common/Services/ReferralProgram/Model/RegisterUserRequest.swift b/p2p_wallet/Common/Services/ReferralProgram/Model/RegisterUserRequest.swift new file mode 100644 index 0000000000..9267c168b9 --- /dev/null +++ b/p2p_wallet/Common/Services/ReferralProgram/Model/RegisterUserRequest.swift @@ -0,0 +1,30 @@ +import Foundation +import SolanaSwift +import TweetNacl + +struct RegisterUserRequest: Encodable { + let user: String + let timedSignature: ReferralTimedSignature + + enum CodingKeys: String, CodingKey { + case user, timedSignature = "timed_signature" + } +} + +struct RegisterUserSignature: BorshSerializable { + let user: String + let referrent: String? + let timestamp: Int64 + + func serialize(to writer: inout Data) throws { + try user.serialize(to: &writer) + try Optional(referrent)?.serialize(to: &writer) + try timestamp.serialize(to: &writer) + } + + func sign(secretKey: Data) throws -> Data { + var data = Data() + try serialize(to: &data) + return try NaclSign.signDetached(message: data, secretKey: secretKey) + } +} diff --git a/p2p_wallet/Common/Services/ReferralProgram/Model/SetReferentRequest.swift b/p2p_wallet/Common/Services/ReferralProgram/Model/SetReferentRequest.swift new file mode 100644 index 0000000000..81a61118bd --- /dev/null +++ b/p2p_wallet/Common/Services/ReferralProgram/Model/SetReferentRequest.swift @@ -0,0 +1,31 @@ +import Foundation +import SolanaSwift +import TweetNacl + +struct SetReferentRequest: Encodable { + let user: String + let referent: String + let timedSignature: ReferralTimedSignature + + enum CodingKeys: String, CodingKey { + case user, referent, timedSignature = "timed_signature" + } +} + +struct SetReferentSignature: BorshSerializable { + let user: String + let referent: String + let timestamp: Int64 + + func serialize(to writer: inout Data) throws { + try user.serialize(to: &writer) + try referent.serialize(to: &writer) + try timestamp.serialize(to: &writer) + } + + func sign(secretKey: Data) throws -> Data { + var data = Data() + try serialize(to: &data) + return try NaclSign.signDetached(message: data, secretKey: secretKey) + } +} diff --git a/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift index e5263957ca..9953ba4333 100644 --- a/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift +++ b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift @@ -9,7 +9,8 @@ protocol ReferralProgramService { var referrer: String { get } var shareLink: URL { get } - func setReferent(from: String) async throws -> String + func register() async + func setReferent(from: String) async } enum ReferralProgramServiceError: Error { @@ -38,43 +39,6 @@ final class ReferralProgramServiceImpl { // MARK: - ReferralProgramService extension ReferralProgramServiceImpl: ReferralProgramService { - private struct SetReferentRequest: Encodable { - struct TimedSignature: Encodable { - let timestamp: Int64 - let signature: String - } - - let user: String - let referent: String - let timedSignature: TimedSignature - - enum CodingKeys: String, CodingKey { - case user, referent, timedSignature = "timed_signature" - } - } - - private struct SetReferentSignature: BorshSerializable { - let user: String - let referent: String - let timestamp: Int64 - - func serialize(to writer: inout Data) throws { - try user.serialize(to: &writer) - try referent.serialize(to: &writer) - try timestamp.serialize(to: &writer) - } - - func sign(secretKey: Data) throws -> Data { - var data = Data() - try serialize(to: &data) - return try NaclSign.signDetached(message: data, secretKey: secretKey) - } - - func signAsBase64(secretKey: Data) throws -> String { - try sign(secretKey: secretKey).base64EncodedString() - } - } - var referrer: String { nameStorage.getName() ?? currentUserAddress } @@ -83,24 +47,68 @@ extension ReferralProgramServiceImpl: ReferralProgramService { URL(string: "https://r.key.app/\(referrer)")! } - func setReferent(from: String) async throws -> String { - guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet } - var timestamp = Int64(Date().timeIntervalSince1970) - let signed = try SetReferentSignature(user: currentUserAddress, referent: referrer, timestamp: timestamp) - .signAsBase64(secretKey: secret) - return try await jsonrpcClient.request( - baseURL: baseURL, - body: .init( - method: "set_referent", - params: SetReferentRequest( - user: currentUserAddress, - referent: from, - timedSignature: SetReferentRequest.TimedSignature( - timestamp: timestamp, - signature: signed + func register() async { + guard !Defaults.referrerRegistered else { return } + do { + guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet } + let timestamp = Int64(Date().timeIntervalSince1970) + let signed = try RegisterUserSignature( + user: currentUserAddress, referrent: nil, timestamp: timestamp + ) + .sign(secretKey: secret) + let _: String = try await jsonrpcClient.request( + baseURL: baseURL, + body: .init( + method: "register", + params: RegisterUserRequest( + user: currentUserAddress, + timedSignature: ReferralTimedSignature( + timestamp: timestamp, signature: signed.toHexString() + ) + ) + ) + ) + Defaults.referrerRegistered = true + } catch { + debugPrint(error) + DefaultLogManager.shared.log( + event: "\(ReferralProgramService.self)_register", + data: error.localizedDescription, + logLevel: LogLevel.error + ) + } + } + + func setReferent(from: String) async { + guard from != currentUserAddress else { return } + do { + guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet } + let timestamp = Int64(Date().timeIntervalSince1970) + let signed = try SetReferentSignature( + user: currentUserAddress, referent: referrer, timestamp: timestamp + ) + .sign(secretKey: secret) + let _: String = try await jsonrpcClient.request( + baseURL: baseURL, + body: .init( + method: "set_referent", + params: SetReferentRequest( + user: currentUserAddress, + referent: from, + timedSignature: ReferralTimedSignature( + timestamp: timestamp, + signature: signed.toHexString() + ) ) ) ) - ) + } catch { + debugPrint(error) + DefaultLogManager.shared.log( + event: "\(ReferralProgramService.self)_setReferent", + data: error.localizedDescription, + logLevel: LogLevel.error + ) + } } } diff --git a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift index 5e9fed5ab6..a5077674b7 100644 --- a/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift +++ b/p2p_wallet/Scenes/Main/ReferralProgram/Details/Bridge/ReferralJSBridge.swift @@ -121,9 +121,8 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { { Task { do { - let signed = try SignMessageSignature(message: message) - .signAsBase64(secretKey: user.account.secretKey) - handler(signed, nil) + let signed = try NaclSign.signDetached(message: base64Data, secretKey: user.account.secretKey) + handler(signed.base64EncodedString(), nil) } catch { handler(nil, .signFailed) } @@ -137,21 +136,3 @@ extension ReferralJSBridge: WKScriptMessageHandlerWithReply { } } } - -struct SignMessageSignature: Codable, BorshSerializable { - let message: String - - func serialize(to writer: inout Data) throws { - try message.serialize(to: &writer) - } - - func sign(secretKey: Data) throws -> Data { - var data = Data() - try serialize(to: &data) - return try NaclSign.signDetached(message: data, secretKey: secretKey) - } - - func signAsBase64(secretKey: Data) throws -> String { - try sign(secretKey: secretKey).base64EncodedString() - } -} diff --git a/p2p_wallet/p2p_wallet.entitlements b/p2p_wallet/p2p_wallet.entitlements index 826fc669b6..ac57a5f3d9 100644 --- a/p2p_wallet/p2p_wallet.entitlements +++ b/p2p_wallet/p2p_wallet.entitlements @@ -10,6 +10,7 @@ com.apple.developer.associated-domains + applinks:r.key.app applinks:keyapp-te.onelink.me applinks:keyapp.onelink.me applinks:t.key.app From a350b09b3c8bce5b69e3ff4caab7bdaef72fc137 Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Thu, 1 Feb 2024 18:09:22 +0400 Subject: [PATCH 22/25] [ETH-831] Fix wrong param name in referral service --- .../DeeplinkAppDelegateService.swift | 2 +- .../Services/ReferralProgram/ReferralProgramService.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift index 94aae150a8..b79909c625 100644 --- a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift +++ b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift @@ -136,7 +136,7 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService { } private func setReferrerIfNeeded(r: String) { -// guard available(.referralProgramEnabled) else { return } + guard available(.referralProgramEnabled) else { return } let referralService: ReferralProgramService = Resolver.resolve() Task { _ = await referralService.setReferent(from: r) diff --git a/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift index 9953ba4333..ee013117e1 100644 --- a/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift +++ b/p2p_wallet/Common/Services/ReferralProgram/ReferralProgramService.swift @@ -56,7 +56,7 @@ extension ReferralProgramServiceImpl: ReferralProgramService { user: currentUserAddress, referrent: nil, timestamp: timestamp ) .sign(secretKey: secret) - let _: String = try await jsonrpcClient.request( + let _: String? = try await jsonrpcClient.request( baseURL: baseURL, body: .init( method: "register", @@ -85,10 +85,10 @@ extension ReferralProgramServiceImpl: ReferralProgramService { guard let secret = userWallet.wallet?.account.secretKey else { throw ReferralProgramServiceError.failedSet } let timestamp = Int64(Date().timeIntervalSince1970) let signed = try SetReferentSignature( - user: currentUserAddress, referent: referrer, timestamp: timestamp + user: currentUserAddress, referent: from, timestamp: timestamp ) .sign(secretKey: secret) - let _: String = try await jsonrpcClient.request( + let _: String? = try await jsonrpcClient.request( baseURL: baseURL, body: .init( method: "set_referent", From 8ef522d4dbf16d82fbec70f974a3912c16a2c98a Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Thu, 1 Feb 2024 18:26:13 +0400 Subject: [PATCH 23/25] [ETH-831] Drop first path component for proper deeplink handle --- .../AppDelegateProxyService/DeeplinkAppDelegateService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift index b79909c625..e285d8e54f 100644 --- a/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift +++ b/p2p_wallet/AppDelegate/AppDelegateProxyService/DeeplinkAppDelegateService.swift @@ -78,7 +78,7 @@ final class DeeplinkAppDelegateService: NSObject, AppDelegateService { } if urlComponents.host == "r.key.app" { - setReferrerIfNeeded(r: urlComponents.path) + setReferrerIfNeeded(r: String(urlComponents.path.dropFirst())) } } From f3225eaed7f4afd00ad4fcce056277a00c79702b Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Fri, 2 Feb 2024 00:03:49 +0400 Subject: [PATCH 24/25] [ETH-831] Clear referrerRegistered flag on logout --- p2p_wallet/Common/Services/Storage/UserWalletManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/p2p_wallet/Common/Services/Storage/UserWalletManager.swift b/p2p_wallet/Common/Services/Storage/UserWalletManager.swift index f1e4e428ab..7584823177 100644 --- a/p2p_wallet/Common/Services/Storage/UserWalletManager.swift +++ b/p2p_wallet/Common/Services/Storage/UserWalletManager.swift @@ -105,6 +105,7 @@ class UserWalletManager: ObservableObject { Defaults.isTokenInputTypeChosen = false Defaults.fromTokenAddress = nil Defaults.toTokenAddress = nil + Defaults.referrerRegistered = false walletSettings.reset() From b344669cad19b56488b0c8c85c77c9476dac37aa Mon Sep 17 00:00:00 2001 From: Elizaveta Semenova Date: Fri, 2 Feb 2024 09:11:36 +0400 Subject: [PATCH 25/25] [ETH-831] Update packages --- Packages/KeyAppKit/Package.swift | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Packages/KeyAppKit/Package.swift b/Packages/KeyAppKit/Package.swift index e9aba20202..c8d5234503 100644 --- a/Packages/KeyAppKit/Package.swift +++ b/Packages/KeyAppKit/Package.swift @@ -128,7 +128,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/p2p-org/solana-swift", branch: "feature/pwn-783"), + .package(url: "https://github.com/p2p-org/solana-swift", branch: "main"), .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.6.0")), .package(url: "https://github.com/Boilertalk/Web3.swift.git", from: "0.6.0"), // .package(url: "https://github.com/trustwallet/wallet-core", branch: "master"), diff --git a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ac4071a4fd..799c3c0299 100644 --- a/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/p2p_wallet.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -311,8 +311,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/p2p-org/solana-swift", "state" : { - "branch" : "feature/pwn-783", - "revision" : "ca0b6b46dab4975a336a848e9e38d75adc3ebab0" + "branch" : "main", + "revision" : "3811cabe260e4b88e8096527a50eee8d23b41204" } }, {