Skip to content

Commit a566f35

Browse files
APPLE: add handle subscription payment (#5010)
1 parent 400b81a commit a566f35

13 files changed

Lines changed: 225 additions & 139 deletions

File tree

nym-vpn-apple/Home/Sources/Home/HomeViewModel.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ private extension HomeViewModel {
266266
setupSystemMessageObservers()
267267
setupIsMnemonicImportedObserver()
268268
setupAccountSummaryObserver()
269+
setupSubscriptionPaymentObserver()
269270
#if os(iOS)
270271
setupConnectionErrorObservers()
271272
setupNetworkMonitorObservers()
@@ -322,6 +323,25 @@ private extension HomeViewModel {
322323
.store(in: &cancellables)
323324
}
324325

326+
func setupSubscriptionPaymentObserver() {
327+
credentialsManager.$didReceiveSubscriptionPayment
328+
.receive(on: DispatchQueue.main)
329+
.sink { [weak self] didReceive in
330+
guard didReceive else { return }
331+
MainActor.assumeIsolated {
332+
self?.credentialsManager.didReceiveSubscriptionPayment = false
333+
self?.path = .init()
334+
self?.messagesManager.addAndProcess(
335+
SnackBarMessage(
336+
text: "subscriptionPayment.received".localizedString,
337+
style: .info
338+
)
339+
)
340+
}
341+
}
342+
.store(in: &cancellables)
343+
}
344+
325345
func setupSystemMessageObservers() {
326346
messagesManager.$currentMessage.sink { [weak self] message in
327347
guard let message

nym-vpn-apple/NymVPN/Resources/Localizable.xcstrings

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24044,24 +24044,24 @@
2404424044
}
2404524045
}
2404624046
},
24047-
"requestingZkNyms" : {
24047+
"noActivePlan" : {
2404824048
"extractionState" : "manual",
2404924049
"localizations" : {
2405024050
"en" : {
2405124051
"stringUnit" : {
2405224052
"state" : "translated",
24053-
"value" : "Requesting ZkNyms..."
24053+
"value" : "No active plan"
2405424054
}
2405524055
}
2405624056
}
2405724057
},
24058-
"noActivePlan" : {
24058+
"subscriptionPayment.received" : {
2405924059
"extractionState" : "manual",
2406024060
"localizations" : {
2406124061
"en" : {
2406224062
"stringUnit" : {
2406324063
"state" : "translated",
24064-
"value" : "No active plan"
24064+
"value" : "Payment received\nRequesting ZkNyms..."
2406524065
}
2406624066
}
2406724067
}
@@ -26783,7 +26783,7 @@
2678326783
"en" : {
2678426784
"stringUnit" : {
2678526785
"state" : "translated",
26786-
"value" : "Pin code"
26786+
"value" : "Passcode"
2678726787
}
2678826788
}
2678926789
}
@@ -30060,6 +30060,17 @@
3006030060
}
3006130061
}
3006230062
},
30063+
"requestingZkNyms" : {
30064+
"extractionState" : "manual",
30065+
"localizations" : {
30066+
"en" : {
30067+
"stringUnit" : {
30068+
"state" : "translated",
30069+
"value" : "Requesting ZkNyms..."
30070+
}
30071+
}
30072+
}
30073+
},
3006330074
"retry" : {
3006430075
"extractionState" : "manual",
3006530076
"localizations" : {
@@ -31289,7 +31300,7 @@
3128931300
"en" : {
3129031301
"stringUnit" : {
3129131302
"state" : "translated",
31292-
"value" : "Fetching pin code"
31303+
"value" : "Fetching passcode"
3129331304
}
3129431305
}
3129531306
}

nym-vpn-apple/Services/Sources/Services/CredentialsManager/CredentialsManager.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import PathManager
3535
public var deviceIdentifier: String?
3636
@Published public var accountIdentifier: String?
3737
@Published public var didReceiveAccountLinkCallback = false
38+
@Published public var didReceiveSubscriptionPayment = false
3839
@Published public var accountSummary: AccountSummary?
3940

4041
public var isValidCredentialImported: Bool {
@@ -185,6 +186,15 @@ import PathManager
185186
didReceiveAccountLinkCallback = true
186187
}
187188

189+
public func handleSubscriptionPayment() async throws {
190+
#if os(macOS)
191+
didReceiveSubscriptionPayment = true
192+
try await grpcManager.handleSubscriptionPayment()
193+
try? await Task.sleep(for: .seconds(2))
194+
await updateAccountSummary()
195+
#endif
196+
}
197+
188198
public func autologin(kind: NymDeeplinkKind) async throws -> (url: String, pinCode: String)? {
189199
let locale = Locale.current.language.languageCode?.identifier.lowercased() ?? "en"
190200
let name = "default"

nym-vpn-apple/Services/Sources/Services/DeeplinkManager/DeeplinkManager.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,17 @@ public final class DeeplinkManager {
2323
try await credentialsManager.storeDeeplink(callbackURLString: url.absoluteString)
2424
}
2525
}
26-
// Create Account response
26+
// Account response
2727
if components.host == "account", components.path == "/response" {
28+
let hasDeeplinkId = components.queryItems?.contains(where: { $0.name == "deeplink_id" }) == true
2829
Task {
29-
try await credentialsManager.storeDeeplink(callbackURLString: url.absoluteString)
30+
if hasDeeplinkId {
31+
// nymvpn://account/response?deeplink_id=...&payload=...
32+
try await credentialsManager.storeDeeplink(callbackURLString: url.absoluteString)
33+
} else {
34+
// nymvpn://account/response (subscription payment)
35+
try await credentialsManager.handleSubscriptionPayment()
36+
}
3037
}
3138
}
3239
}

nym-vpn-apple/ServicesMacOS/Sources/GRPCManager/GRPCManager+Credentials.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ extension GRPCManager {
6868
}.value
6969
}
7070

71+
public func handleSubscriptionPayment() async throws {
72+
try await Task.detached { [weak self] in
73+
try await self?.rpcClient?.handleSubscriptionPayment()
74+
}.value
75+
}
76+
7177
public func autologin(
7278
locale: String,
7379
name: String,

nym-vpn-apple/Settings/Sources/Settings/AccountAndDevices/AccountAndDevicesView+DialogConfigurations.swift

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,6 @@ import Theme
44

55
// MARK: - Dialog Configurations -
66
extension AccountAndDevicesView {
7-
var autologinLoadingConfiguration: ActionDialogConfiguration {
8-
ActionDialogConfiguration(
9-
titleLocalizedString: "settings.account.autologin.fetchingPinCode".localizedString,
10-
noLocalizedString: "cancel".localizedString,
11-
noAction: {
12-
autologinTask?.cancel()
13-
autologinTask = nil
14-
isAutologinLoading = false
15-
},
16-
loadingText: "settings.account.autologin.loading".localizedString,
17-
shouldCloseAfterYesAction: false
18-
)
19-
}
20-
21-
var autologinErrorConfiguration: ActionDialogConfiguration {
22-
ActionDialogConfiguration(
23-
systemIconImageName: "exclamationmark.triangle",
24-
systemIconImageColor: NymColor.error,
25-
titleLocalizedString: "generalNymError.somethingWentWrong".localizedString,
26-
subtitleLocalizedString: autologinErrorMessage,
27-
yesLocalizedString: "settings.account.autologin.retry".localizedString,
28-
noLocalizedString: "cancel".localizedString,
29-
yesAction: {
30-
isAutologinError = false
31-
navigateToAccount()
32-
},
33-
shouldCloseAfterYesAction: false,
34-
verticalButtonsLayout: true
35-
)
36-
}
37-
387
var logoutDialogConfiguration: ActionDialogConfiguration {
398
ActionDialogConfiguration(
409
systemIconImageName: "rectangle.portrait.and.arrow.right",

nym-vpn-apple/Settings/Sources/Settings/AccountAndDevices/AccountAndDevicesView.swift

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,8 @@ import Theme
2525
@State private var isPresentedManageSubscription = false
2626
@State var isLogoutConfirmationDisplayed = false
2727
@State var isLogoutLoading = false
28-
@State var isPinCodeDisplayed = false
2928
@State var isLinkAccountAvailable = false
30-
@State var pinCode: String = ""
31-
@State var isAutologinLoading = false
32-
@State var isAutologinError = false
33-
@State var autologinErrorMessage = ""
34-
@State var autologinURL = ""
35-
@State var autologinTask: Task<Void, Never>?
29+
@State var autologinState = AutologinState()
3630

3731
@Binding private var path: NavigationPath
3832

@@ -75,15 +69,7 @@ import Theme
7569
#if os(iOS)
7670
.manageSubscriptionsSheet(isPresented: $isPresentedManageSubscription)
7771
#endif
78-
.overlay {
79-
if isPinCodeDisplayed, !pinCode.isEmpty {
80-
PinCodeView(
81-
isDisplayed: $isPinCodeDisplayed,
82-
pinCode: $pinCode,
83-
url: $autologinURL
84-
)
85-
}
86-
}
72+
.autologinOverlay(state: autologinState, onRetry: { navigateToAccount() })
8773
.background {
8874
NymColor.background
8975
.ignoresSafeArea()
@@ -100,29 +86,6 @@ import Theme
10086
)
10187
}
10288
}
103-
.overlay {
104-
if isAutologinLoading {
105-
ActionDialogView(
106-
viewModel: ActionDialogViewModel(
107-
isDisplayed: $isAutologinLoading,
108-
configuration: autologinLoadingConfiguration,
109-
impactGenerator: .shared,
110-
isLoading: .constant(true)
111-
)
112-
)
113-
}
114-
}
115-
.overlay {
116-
if isAutologinError {
117-
ActionDialogView(
118-
viewModel: ActionDialogViewModel(
119-
isDisplayed: $isAutologinError,
120-
configuration: autologinErrorConfiguration,
121-
impactGenerator: .shared
122-
)
123-
)
124-
}
125-
}
12689
.task {
12790
await updateIsAccountLinkAvailable()
12891
}
@@ -338,45 +301,18 @@ extension AccountAndDevicesView {
338301

339302
func navigateToAccount() {
340303
impactGenerator.softImpact()
341-
isAutologinLoading = true
342-
343-
autologinTask = Task {
344-
await autologin(kind: .autologinView)
345-
}
304+
autologinState.start(kind: .autologinView, using: credentialsManager)
346305
}
347306

348307
func navigateToPlanPurchase() {
349308
impactGenerator.softImpact()
350309
#if os(iOS)
351310
path.append(SettingLink.generatePassphrase(displayPurchaseView: true))
352311
#elseif os(macOS)
353-
isAutologinLoading = true
354-
355-
autologinTask = Task {
356-
await autologin(kind: .autologinRenew)
357-
}
312+
autologinState.start(kind: .autologinRenew, using: credentialsManager)
358313
#endif
359314
}
360315

361-
func autologin(kind: NymDeeplinkKind) async {
362-
do {
363-
guard let result = try await credentialsManager.autologin(kind: kind) else {
364-
isAutologinLoading = false
365-
return
366-
}
367-
isAutologinLoading = false
368-
pinCode = result.pinCode
369-
autologinURL = result.url
370-
isPinCodeDisplayed = true
371-
} catch is CancellationError {
372-
isAutologinLoading = false
373-
} catch {
374-
isAutologinLoading = false
375-
autologinErrorMessage = error.localizedDescription
376-
isAutologinError = true
377-
}
378-
}
379-
380316
func linkAccount() async {
381317
impactGenerator.softImpact()
382318
let link = try? await credentialsManager.privyLogin(kind: .privyLink)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import SwiftUI
2+
import ImpactGenerator
3+
import UIComponents
4+
import Theme
5+
6+
struct AutologinOverlay: ViewModifier {
7+
var autologinState: AutologinState
8+
var onRetry: (() -> Void)?
9+
10+
func body(content: Content) -> some View {
11+
@Bindable var autologin = autologinState
12+
content
13+
.overlay {
14+
if autologinState.isPinCodeDisplayed, !autologinState.pinCode.isEmpty {
15+
PinCodeView(
16+
isDisplayed: $autologin.isPinCodeDisplayed,
17+
pinCode: $autologin.pinCode,
18+
url: $autologin.url
19+
)
20+
}
21+
}
22+
.overlay {
23+
if autologinState.isLoading {
24+
ActionDialogView(
25+
viewModel: ActionDialogViewModel(
26+
isDisplayed: $autologin.isLoading,
27+
configuration: loadingConfiguration,
28+
impactGenerator: .shared,
29+
isLoading: .constant(true)
30+
)
31+
)
32+
}
33+
}
34+
.overlay {
35+
if autologinState.isError {
36+
ActionDialogView(
37+
viewModel: ActionDialogViewModel(
38+
isDisplayed: $autologin.isError,
39+
configuration: errorConfiguration,
40+
impactGenerator: .shared
41+
)
42+
)
43+
}
44+
}
45+
}
46+
47+
private var loadingConfiguration: ActionDialogConfiguration {
48+
ActionDialogConfiguration(
49+
titleLocalizedString: "settings.account.autologin.fetchingPinCode".localizedString,
50+
noLocalizedString: "cancel".localizedString,
51+
noAction: {
52+
autologinState.cancel()
53+
},
54+
loadingText: "settings.account.autologin.loading".localizedString,
55+
shouldCloseAfterYesAction: false
56+
)
57+
}
58+
59+
private var errorConfiguration: ActionDialogConfiguration {
60+
if let onRetry {
61+
ActionDialogConfiguration(
62+
systemIconImageName: "exclamationmark.triangle",
63+
systemIconImageColor: NymColor.error,
64+
titleLocalizedString: "generalNymError.somethingWentWrong".localizedString,
65+
subtitleLocalizedString: autologinState.errorMessage,
66+
yesLocalizedString: "settings.account.autologin.retry".localizedString,
67+
noLocalizedString: "cancel".localizedString,
68+
yesAction: {
69+
autologinState.isError = false
70+
onRetry()
71+
},
72+
shouldCloseAfterYesAction: false,
73+
verticalButtonsLayout: true
74+
)
75+
} else {
76+
ActionDialogConfiguration(
77+
systemIconImageName: "exclamationmark.triangle",
78+
systemIconImageColor: NymColor.error,
79+
titleLocalizedString: "generalNymError.somethingWentWrong".localizedString,
80+
subtitleLocalizedString: autologinState.errorMessage,
81+
noLocalizedString: "cancel".localizedString
82+
)
83+
}
84+
}
85+
}
86+
87+
extension View {
88+
func autologinOverlay(state: AutologinState, onRetry: (() -> Void)? = nil) -> some View {
89+
modifier(AutologinOverlay(autologinState: state, onRetry: onRetry))
90+
}
91+
}

0 commit comments

Comments
 (0)