Skip to content

Commit ed1a8ff

Browse files
authored
Merge pull request #560 from synonymdev/codex/paykit-feature-flag-followups
fix: gate paykit ui
2 parents c930d2d + 7a2025d commit ed1a8ff

20 files changed

Lines changed: 431 additions & 65 deletions

Bitkit/AppScene.swift

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ struct AppScene: View {
5454

5555
// Run app data migrations before any feature code loads migrated state
5656
AppDataMigrations.run()
57+
PaykitFeatureFlags.enforceBuildAvailability()
5758

5859
_app = StateObject(wrappedValue: AppViewModel(sheetViewModel: sheetViewModel, navigationViewModel: navigationViewModel))
5960
_sheets = StateObject(wrappedValue: sheetViewModel)
@@ -148,13 +149,21 @@ struct AppScene: View {
148149
.environment(trezorViewModel)
149150
.onChange(of: pubkyProfile.authState, initial: true) { _, authState in
150151
if authState == .authenticated, let pk = pubkyProfile.publicKey {
151-
Task { try? await contactsManager.loadContacts(for: pk) }
152+
Task {
153+
try? await contactsManager.loadContacts(for: pk)
154+
if !PaykitFeatureFlags.isUIEnabled, wallet.walletExists == true {
155+
await retryPendingPaykitEndpointRemoval()
156+
}
157+
}
152158
} else if authState == .idle {
153159
contactsManager.reset()
154160
}
155161
}
156162
.onReceive(contactsManager.$contacts) { contacts in
157-
guard wallet.walletExists == true, pubkyProfile.authState == .authenticated else { return }
163+
guard PaykitFeatureFlags.isUIEnabled,
164+
wallet.walletExists == true,
165+
pubkyProfile.authState == .authenticated
166+
else { return }
158167
let publicKeys = contacts.map(\.publicKey)
159168
Task {
160169
await PrivatePaykitService.shared.prepareSavedContacts(publicKeys, wallet: wallet)
@@ -385,6 +394,9 @@ struct AppScene: View {
385394
do {
386395
try await wallet.start()
387396
try await activity.syncLdkNodePayments()
397+
if !PaykitFeatureFlags.isUIEnabled {
398+
await retryPendingPaykitEndpointRemoval()
399+
}
388400

389401
// Start watching pending orders after wallet is ready
390402
await blocktank.startWatchingPendingOrders(transferViewModel: transfer)
@@ -546,6 +558,10 @@ struct AppScene: View {
546558
app.markAppStatusInit()
547559
BackupService.shared.startObservingBackups()
548560
Task {
561+
if !PaykitFeatureFlags.isUIEnabled {
562+
await retryPendingPaykitEndpointRemoval()
563+
}
564+
guard PaykitFeatureFlags.isUIEnabled else { return }
549565
await PrivatePaykitAddressReservationStore.shared.reconcileReservedIndexesWithLdk()
550566
await PrivatePaykitService.shared.prepareSavedContacts(
551567
contactsManager.contacts.map(\.publicKey),
@@ -578,20 +594,26 @@ struct AppScene: View {
578594
await clearDeliveredNotifications()
579595
await LightningService.shared.reconnectPeers()
580596
try? await wallet.sync()
581-
await PrivatePaykitService.shared.retryPendingEndpointRemoval(
582-
wallet: wallet,
583-
savedPublicKeys: contactsManager.contacts.map(\.publicKey)
584-
)
597+
await retryPendingPaykitEndpointRemoval()
585598
await wallet.refreshPublicPaykitEndpointsOnForeground()
586-
await PrivatePaykitService.shared.refreshSavedContactEndpoints(
587-
for: contactsManager.contacts.map(\.publicKey),
588-
wallet: wallet
589-
)
599+
if PaykitFeatureFlags.isUIEnabled {
600+
await PrivatePaykitService.shared.refreshSavedContactEndpoints(
601+
for: contactsManager.contacts.map(\.publicKey),
602+
wallet: wallet
603+
)
604+
}
590605
}
591606
}
592607
}
593608
}
594609

610+
private func retryPendingPaykitEndpointRemoval() async {
611+
await PrivatePaykitService.shared.retryPendingEndpointRemoval(
612+
wallet: wallet,
613+
savedPublicKeys: contactsManager.contacts.map(\.publicKey)
614+
)
615+
}
616+
595617
/// Removes all delivered notifications from Notification Center so the app can handle them when opened.
596618
private func clearDeliveredNotifications() async {
597619
let center = UNUserNotificationCenter.current()

Bitkit/Components/Activity/ActivityList.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import BitkitCore
22
import SwiftUI
33

44
struct ActivityList: View {
5+
@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false
6+
57
@EnvironmentObject var activity: ActivityListViewModel
68
@EnvironmentObject var contactsManager: ContactsManager
79
@EnvironmentObject var feeEstimatesManager: FeeEstimatesManager
810

911
let viewType: ActivityViewType
1012

13+
private var isPaykitUIActive: Bool {
14+
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
15+
}
16+
1117
enum ActivityViewType {
1218
case all
1319
case lightning
@@ -31,7 +37,7 @@ struct ActivityList: View {
3137
ActivityRow(
3238
item: item,
3339
feeEstimates: feeEstimatesManager.estimates,
34-
contact: item.contact(in: contactsManager.contacts)
40+
contact: isPaykitUIActive ? item.contact(in: contactsManager.contacts) : nil
3541
)
3642
}
3743
.accessibilityIdentifier("Activity-\(index)")

Bitkit/Components/Header.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import SwiftUI
22

33
struct Header: View {
4+
@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false
5+
46
@EnvironmentObject var app: AppViewModel
57
@EnvironmentObject var navigation: NavigationViewModel
68
@EnvironmentObject var pubkyProfile: PubkyProfileManager
@@ -10,14 +12,20 @@ struct Header: View {
1012
/// Binding to widgets edit state; used when showWidgetEditButton is true.
1113
@Binding var isEditingWidgets: Bool
1214

15+
private var isPaykitUIActive: Bool {
16+
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
17+
}
18+
1319
init(showWidgetEditButton: Bool = false, isEditingWidgets: Binding<Bool> = .constant(false)) {
1420
self.showWidgetEditButton = showWidgetEditButton
1521
_isEditingWidgets = isEditingWidgets
1622
}
1723

1824
var body: some View {
1925
HStack(alignment: .center, spacing: 0) {
20-
profileButton
26+
if isPaykitUIActive {
27+
profileButton
28+
}
2129

2230
Spacer()
2331

@@ -65,7 +73,6 @@ struct Header: View {
6573
.padding(.trailing, 10)
6674
}
6775

68-
@ViewBuilder
6976
private var profileButton: some View {
7077
Button {
7178
if pubkyProfile.isAuthenticated || pubkyProfile.cachedName != nil {

Bitkit/Components/Widgets/Suggestions.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,13 @@ struct Suggestions: View {
172172
@EnvironmentObject var wallet: WalletViewModel
173173
@EnvironmentObject var pubkyProfile: PubkyProfileManager
174174

175+
@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false
175176
@State private var showShareSheet = false
176177

178+
private var isPaykitUIActive: Bool {
179+
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
180+
}
181+
177182
/// Which suggestion cards to show.
178183
/// Up to 4 for current wallet state, in priority order; completed and dismissed are skipped.
179184
/// In widget preview: 2 fixed cards.
@@ -183,6 +188,7 @@ struct Suggestions: View {
183188
settings: SettingsViewModel,
184189
suggestionsManager: SuggestionsManager,
185190
pubkyProfile: PubkyProfileManager? = nil,
191+
isPaykitUIEnabled: Bool = PaykitFeatureFlags.isUIEnabled,
186192
isPreview: Bool = false
187193
) -> [SuggestionCardData] {
188194
if isPreview {
@@ -199,6 +205,7 @@ struct Suggestions: View {
199205
var result: [SuggestionCardData] = []
200206
for id in orderedIds {
201207
guard let card = cardsById[id] else { continue }
208+
if !isPaykitUIEnabled, card.isPaykitCard { continue }
202209
if isCardCompleted(card, app: app, settings: settings, pubkyProfile: pubkyProfile) { continue }
203210
if suggestionsManager.isDismissed(card.id) { continue }
204211
result.append(card)
@@ -229,6 +236,7 @@ struct Suggestions: View {
229236
settings: settings,
230237
suggestionsManager: suggestionsManager,
231238
pubkyProfile: pubkyProfile,
239+
isPaykitUIEnabled: isPaykitUIActive,
232240
isPreview: isPreview
233241
)
234242
}
@@ -273,6 +281,7 @@ struct Suggestions: View {
273281
}
274282

275283
private func onItemTap(_ card: SuggestionCardData) {
284+
if card.isPaykitCard, !PaykitFeatureFlags.isUIEnabled { return }
276285
var route: Route?
277286

278287
switch card.action {
@@ -322,6 +331,12 @@ struct Suggestions: View {
322331
}
323332
}
324333

334+
private extension SuggestionCardData {
335+
var isPaykitCard: Bool {
336+
action == .profile
337+
}
338+
}
339+
325340
#Preview {
326341
VStack {
327342
Suggestions()
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Foundation
2+
3+
enum PaykitFeatureFlags {
4+
static let uiEnabledKey = "paykitUiEnabled"
5+
6+
static var isUIAvailable: Bool {
7+
#if FEATURE_PAYKIT_UI_DISABLED
8+
false
9+
#else
10+
true
11+
#endif
12+
}
13+
14+
static var isUIEnabled: Bool {
15+
isUIAvailable && UserDefaults.standard.bool(forKey: uiEnabledKey)
16+
}
17+
18+
static func enforceBuildAvailability() {
19+
let defaults = UserDefaults.standard
20+
let hasPublishedState = defaults.bool(forKey: PublicPaykitService.publishingEnabledKey) ||
21+
defaults.bool(forKey: PrivatePaykitService.publishingEnabledKey) ||
22+
defaults.bool(forKey: "hasConfirmedPublicPaykitEndpoints") ||
23+
!(defaults.string(forKey: "publicPaykitBolt11") ?? "").isEmpty
24+
25+
guard !isUIEnabled, hasPublishedState else { return }
26+
27+
defaults.set(false, forKey: uiEnabledKey)
28+
defaults.set(false, forKey: "hasConfirmedPublicPaykitEndpoints")
29+
defaults.set(false, forKey: PublicPaykitService.publishingEnabledKey)
30+
defaults.set(false, forKey: PrivatePaykitService.publishingEnabledKey)
31+
defaults.removeObject(forKey: "publicPaykitBolt11")
32+
defaults.removeObject(forKey: "publicPaykitBolt11PaymentHash")
33+
defaults.removeObject(forKey: "publicPaykitBolt11ExpiresAt")
34+
35+
PrivatePaykitService.setContactSharingCleanupPending(true)
36+
}
37+
}

Bitkit/MainNavView.swift

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import SwiftUI
22

33
struct MainNavView: View {
4+
@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false
5+
46
@EnvironmentObject private var app: AppViewModel
57
@Environment(CameraManager.self) private var cameraManager
68
@EnvironmentObject private var contactsManager: ContactsManager
@@ -16,6 +18,10 @@ struct MainNavView: View {
1618
@State private var showClipboardAlert = false
1719
@State private var clipboardUri: String?
1820

21+
private var isPaykitUIActive: Bool {
22+
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
23+
}
24+
1925
// Delay constants for clipboard processing
2026
private static let nodeReadyDelayNanoseconds: UInt64 = 500_000_000 // 0.5 seconds
2127
private static let statePropagationDelayNanoseconds: UInt64 = 500_000_000 // 0.5 seconds
@@ -247,6 +253,15 @@ struct MainNavView: View {
247253
Logger.info("Received deeplink: \(url.absoluteString)")
248254

249255
if let callback = PubkyRingAuthCallback.parse(url: url) {
256+
guard isPaykitUIActive else {
257+
app.toast(
258+
type: .error,
259+
title: t("profile__auth_error_title"),
260+
description: t("other__qr_error_text")
261+
)
262+
return
263+
}
264+
250265
let handlingResult = await pubkyProfile.handleAuthCallback(callback)
251266

252267
switch handlingResult {
@@ -368,7 +383,9 @@ struct MainNavView: View {
368383

369384
// Profile & Contacts
370385
case .contacts:
371-
if let initializationErrorMessage = pubkyProfile.initializationErrorMessage {
386+
if !isPaykitUIActive {
387+
ComingSoonScreen()
388+
} else if let initializationErrorMessage = pubkyProfile.initializationErrorMessage {
372389
pubkyInitializationErrorView(message: initializationErrorMessage)
373390
} else if app.hasSeenContactsIntro || !contactsManager.contacts.isEmpty {
374391
if !pubkyProfile.isInitialized {
@@ -383,12 +400,18 @@ struct MainNavView: View {
383400
} else {
384401
ContactsIntroView()
385402
}
386-
case .contactsIntro: ContactsIntroView()
387-
case let .contactDetail(publicKey): ContactDetailView(publicKey: publicKey)
388-
case let .contactActivity(publicKey): ContactActivityView(publicKey: publicKey)
389-
case let .assignActivityContact(activityId): AssignActivityContactView(activityId: activityId)
403+
case .contactsIntro:
404+
if isPaykitUIActive { ContactsIntroView() } else { ComingSoonScreen() }
405+
case let .contactDetail(publicKey):
406+
if isPaykitUIActive { ContactDetailView(publicKey: publicKey) } else { paykitDisabledRedirectView }
407+
case let .contactActivity(publicKey):
408+
if isPaykitUIActive { ContactActivityView(publicKey: publicKey) } else { paykitDisabledRedirectView }
409+
case let .assignActivityContact(activityId):
410+
if isPaykitUIActive { AssignActivityContactView(activityId: activityId) } else { paykitDisabledRedirectView }
390411
case .contactImportOverview:
391-
if let fallbackRoute = fallbackRouteForMissingPendingImport(hasPendingImport: contactsManager.hasPendingImport) {
412+
if !isPaykitUIActive {
413+
paykitDisabledRedirectView
414+
} else if let fallbackRoute = fallbackRouteForMissingPendingImport(hasPendingImport: contactsManager.hasPendingImport) {
392415
missingPendingImportView(fallbackRoute: fallbackRoute)
393416
} else if let profile = contactsManager.pendingImportProfile {
394417
ContactImportOverviewView(
@@ -399,15 +422,21 @@ struct MainNavView: View {
399422
missingPendingImportView(fallbackRoute: .payContacts)
400423
}
401424
case .contactImportSelect:
402-
if let fallbackRoute = fallbackRouteForMissingPendingImport(hasPendingImport: contactsManager.hasPendingImport) {
425+
if !isPaykitUIActive {
426+
paykitDisabledRedirectView
427+
} else if let fallbackRoute = fallbackRouteForMissingPendingImport(hasPendingImport: contactsManager.hasPendingImport) {
403428
missingPendingImportView(fallbackRoute: fallbackRoute)
404429
} else {
405430
ContactImportSelectView(contacts: contactsManager.pendingImportContacts)
406431
}
407-
case let .addContact(publicKey): AddContactView(publicKey: publicKey)
408-
case let .editContact(publicKey): EditContactView(publicKey: publicKey)
432+
case let .addContact(publicKey):
433+
if isPaykitUIActive { AddContactView(publicKey: publicKey) } else { paykitDisabledRedirectView }
434+
case let .editContact(publicKey):
435+
if isPaykitUIActive { EditContactView(publicKey: publicKey) } else { paykitDisabledRedirectView }
409436
case .profile:
410-
if let initializationErrorMessage = pubkyProfile.initializationErrorMessage {
437+
if !isPaykitUIActive {
438+
ComingSoonScreen()
439+
} else if let initializationErrorMessage = pubkyProfile.initializationErrorMessage {
411440
pubkyInitializationErrorView(message: initializationErrorMessage)
412441
} else if !pubkyProfile.isInitialized {
413442
pubkyLoadingView
@@ -418,11 +447,16 @@ struct MainNavView: View {
418447
} else {
419448
ProfileIntroView()
420449
}
421-
case .profileIntro: ProfileIntroView()
422-
case .pubkyChoice: PubkyChoiceView()
423-
case .createProfile: CreateProfileView()
424-
case .editProfile: EditProfileView()
425-
case .payContacts: PayContactsView()
450+
case .profileIntro:
451+
if isPaykitUIActive { ProfileIntroView() } else { ComingSoonScreen() }
452+
case .pubkyChoice:
453+
if isPaykitUIActive { PubkyChoiceView() } else { paykitDisabledRedirectView }
454+
case .createProfile:
455+
if isPaykitUIActive { CreateProfileView() } else { paykitDisabledRedirectView }
456+
case .editProfile:
457+
if isPaykitUIActive { EditProfileView() } else { paykitDisabledRedirectView }
458+
case .payContacts:
459+
if isPaykitUIActive { PayContactsView() } else { paykitDisabledRedirectView }
426460

427461
// Shop
428462
case .shopIntro: ShopIntro()
@@ -451,7 +485,8 @@ struct MainNavView: View {
451485
case .widgetsSettings: WidgetsSettingsScreen()
452486
case .notifications: NotificationsSettings()
453487
case .notificationsIntro: NotificationsIntro()
454-
case .paymentPreference: PaymentPreferenceView()
488+
case .paymentPreference:
489+
if isPaykitUIActive { PaymentPreferenceView() } else { paykitDisabledRedirectView }
455490

456491
// Security settings
457492
case .changePin: ChangePinScreen()
@@ -499,6 +534,13 @@ struct MainNavView: View {
499534
}
500535
}
501536

537+
private var paykitDisabledRedirectView: some View {
538+
Color.customBlack
539+
.task {
540+
navigation.reset()
541+
}
542+
}
543+
502544
private func handleClipboard() {
503545
Task { @MainActor in
504546
guard let uri = UIPasteboard.general.string else {

0 commit comments

Comments
 (0)