Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct AppScene: View {

// Run app data migrations before any feature code loads migrated state
AppDataMigrations.run()
PaykitFeatureFlags.enforceBuildAvailability()

_app = StateObject(wrappedValue: AppViewModel(sheetViewModel: sheetViewModel, navigationViewModel: navigationViewModel))
_sheets = StateObject(wrappedValue: sheetViewModel)
Expand Down Expand Up @@ -146,13 +147,21 @@ struct AppScene: View {
.environment(keyboardManager)
.onChange(of: pubkyProfile.authState, initial: true) { _, authState in
if authState == .authenticated, let pk = pubkyProfile.publicKey {
Task { try? await contactsManager.loadContacts(for: pk) }
Task {
try? await contactsManager.loadContacts(for: pk)
if !PaykitFeatureFlags.isUIEnabled, wallet.walletExists == true {
await retryPendingPaykitEndpointRemoval()
}
}
} else if authState == .idle {
contactsManager.reset()
}
}
.onReceive(contactsManager.$contacts) { contacts in
guard wallet.walletExists == true, pubkyProfile.authState == .authenticated else { return }
guard PaykitFeatureFlags.isUIEnabled,
wallet.walletExists == true,
pubkyProfile.authState == .authenticated
else { return }
let publicKeys = contacts.map(\.publicKey)
Task {
await PrivatePaykitService.shared.prepareSavedContacts(publicKeys, wallet: wallet)
Expand Down Expand Up @@ -383,6 +392,9 @@ struct AppScene: View {
do {
try await wallet.start()
try await activity.syncLdkNodePayments()
if !PaykitFeatureFlags.isUIEnabled {
await retryPendingPaykitEndpointRemoval()
}

// Start watching pending orders after wallet is ready
await blocktank.startWatchingPendingOrders(transferViewModel: transfer)
Expand Down Expand Up @@ -544,6 +556,10 @@ struct AppScene: View {
app.markAppStatusInit()
BackupService.shared.startObservingBackups()
Task {
if !PaykitFeatureFlags.isUIEnabled {
await retryPendingPaykitEndpointRemoval()
}
guard PaykitFeatureFlags.isUIEnabled else { return }
await PrivatePaykitAddressReservationStore.shared.reconcileReservedIndexesWithLdk()
await PrivatePaykitService.shared.prepareSavedContacts(
contactsManager.contacts.map(\.publicKey),
Expand Down Expand Up @@ -576,20 +592,26 @@ struct AppScene: View {
await clearDeliveredNotifications()
await LightningService.shared.reconnectPeers()
try? await wallet.sync()
await PrivatePaykitService.shared.retryPendingEndpointRemoval(
wallet: wallet,
savedPublicKeys: contactsManager.contacts.map(\.publicKey)
)
await retryPendingPaykitEndpointRemoval()
await wallet.refreshPublicPaykitEndpointsOnForeground()
await PrivatePaykitService.shared.refreshSavedContactEndpoints(
for: contactsManager.contacts.map(\.publicKey),
wallet: wallet
)
if PaykitFeatureFlags.isUIEnabled {
await PrivatePaykitService.shared.refreshSavedContactEndpoints(
for: contactsManager.contacts.map(\.publicKey),
wallet: wallet
)
}
}
}
}
}

private func retryPendingPaykitEndpointRemoval() async {
await PrivatePaykitService.shared.retryPendingEndpointRemoval(
wallet: wallet,
savedPublicKeys: contactsManager.contacts.map(\.publicKey)
)
}

/// Removes all delivered notifications from Notification Center so the app can handle them when opened.
private func clearDeliveredNotifications() async {
let center = UNUserNotificationCenter.current()
Expand Down
8 changes: 7 additions & 1 deletion Bitkit/Components/Activity/ActivityList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import BitkitCore
import SwiftUI

struct ActivityList: View {
@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false

@EnvironmentObject var activity: ActivityListViewModel
@EnvironmentObject var contactsManager: ContactsManager
@EnvironmentObject var feeEstimatesManager: FeeEstimatesManager

let viewType: ActivityViewType

private var isPaykitUIActive: Bool {
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
}

enum ActivityViewType {
case all
case lightning
Expand All @@ -31,7 +37,7 @@ struct ActivityList: View {
ActivityRow(
item: item,
feeEstimates: feeEstimatesManager.estimates,
contact: item.contact(in: contactsManager.contacts)
contact: isPaykitUIActive ? item.contact(in: contactsManager.contacts) : nil
)
}
.accessibilityIdentifier("Activity-\(index)")
Expand Down
11 changes: 9 additions & 2 deletions Bitkit/Components/Header.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import SwiftUI

struct Header: View {
@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false

@EnvironmentObject var app: AppViewModel
@EnvironmentObject var navigation: NavigationViewModel
@EnvironmentObject var pubkyProfile: PubkyProfileManager
Expand All @@ -10,14 +12,20 @@ struct Header: View {
/// Binding to widgets edit state; used when showWidgetEditButton is true.
@Binding var isEditingWidgets: Bool

private var isPaykitUIActive: Bool {
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
}

init(showWidgetEditButton: Bool = false, isEditingWidgets: Binding<Bool> = .constant(false)) {
self.showWidgetEditButton = showWidgetEditButton
_isEditingWidgets = isEditingWidgets
}

var body: some View {
HStack(alignment: .center, spacing: 0) {
profileButton
if isPaykitUIActive {
profileButton
}

Spacer()

Expand Down Expand Up @@ -65,7 +73,6 @@ struct Header: View {
.padding(.trailing, 10)
}

@ViewBuilder
private var profileButton: some View {
Button {
if pubkyProfile.isAuthenticated || pubkyProfile.cachedName != nil {
Expand Down
15 changes: 15 additions & 0 deletions Bitkit/Components/Widgets/Suggestions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,13 @@ struct Suggestions: View {
@EnvironmentObject var wallet: WalletViewModel
@EnvironmentObject var pubkyProfile: PubkyProfileManager

@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false
@State private var showShareSheet = false

private var isPaykitUIActive: Bool {
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
}

/// Which suggestion cards to show.
/// Up to 4 for current wallet state, in priority order; completed and dismissed are skipped.
/// In widget preview: 2 fixed cards.
Expand All @@ -183,6 +188,7 @@ struct Suggestions: View {
settings: SettingsViewModel,
suggestionsManager: SuggestionsManager,
pubkyProfile: PubkyProfileManager? = nil,
isPaykitUIEnabled: Bool = PaykitFeatureFlags.isUIEnabled,
isPreview: Bool = false
) -> [SuggestionCardData] {
if isPreview {
Expand All @@ -199,6 +205,7 @@ struct Suggestions: View {
var result: [SuggestionCardData] = []
for id in orderedIds {
guard let card = cardsById[id] else { continue }
if !isPaykitUIEnabled, card.isPaykitCard { continue }
if isCardCompleted(card, app: app, settings: settings, pubkyProfile: pubkyProfile) { continue }
if suggestionsManager.isDismissed(card.id) { continue }
result.append(card)
Expand Down Expand Up @@ -229,6 +236,7 @@ struct Suggestions: View {
settings: settings,
suggestionsManager: suggestionsManager,
pubkyProfile: pubkyProfile,
isPaykitUIEnabled: isPaykitUIActive,
isPreview: isPreview
)
}
Expand Down Expand Up @@ -273,6 +281,7 @@ struct Suggestions: View {
}

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

switch card.action {
Expand Down Expand Up @@ -322,6 +331,12 @@ struct Suggestions: View {
}
}

private extension SuggestionCardData {
var isPaykitCard: Bool {
action == .profile
}
}

#Preview {
VStack {
Suggestions()
Expand Down
37 changes: 37 additions & 0 deletions Bitkit/FeatureFlags/PaykitFeatureFlags.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation

enum PaykitFeatureFlags {
static let uiEnabledKey = "paykitUiEnabled"

static var isUIAvailable: Bool {
#if FEATURE_PAYKIT_UI_DISABLED
false
#else
true
#endif
}

static var isUIEnabled: Bool {
isUIAvailable && UserDefaults.standard.bool(forKey: uiEnabledKey)
}

static func enforceBuildAvailability() {
let defaults = UserDefaults.standard
let hasPublishedState = defaults.bool(forKey: PublicPaykitService.publishingEnabledKey) ||
defaults.bool(forKey: PrivatePaykitService.publishingEnabledKey) ||
defaults.bool(forKey: "hasConfirmedPublicPaykitEndpoints") ||
!(defaults.string(forKey: "publicPaykitBolt11") ?? "").isEmpty

guard !isUIEnabled, hasPublishedState else { return }

defaults.set(false, forKey: uiEnabledKey)
defaults.set(false, forKey: "hasConfirmedPublicPaykitEndpoints")
defaults.set(false, forKey: PublicPaykitService.publishingEnabledKey)
defaults.set(false, forKey: PrivatePaykitService.publishingEnabledKey)
defaults.removeObject(forKey: "publicPaykitBolt11")
defaults.removeObject(forKey: "publicPaykitBolt11PaymentHash")
defaults.removeObject(forKey: "publicPaykitBolt11ExpiresAt")

PrivatePaykitService.setContactSharingCleanupPending(true)
}
}
74 changes: 58 additions & 16 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import SwiftUI

struct MainNavView: View {
@AppStorage(PaykitFeatureFlags.uiEnabledKey) private var isPaykitUIEnabled = false

@EnvironmentObject private var app: AppViewModel
@Environment(CameraManager.self) private var cameraManager
@EnvironmentObject private var contactsManager: ContactsManager
Expand All @@ -16,6 +18,10 @@ struct MainNavView: View {
@State private var showClipboardAlert = false
@State private var clipboardUri: String?

private var isPaykitUIActive: Bool {
PaykitFeatureFlags.isUIAvailable && isPaykitUIEnabled
}

// Delay constants for clipboard processing
private static let nodeReadyDelayNanoseconds: UInt64 = 500_000_000 // 0.5 seconds
private static let statePropagationDelayNanoseconds: UInt64 = 500_000_000 // 0.5 seconds
Expand Down Expand Up @@ -247,6 +253,15 @@ struct MainNavView: View {
Logger.info("Received deeplink: \(url.absoluteString)")

if let callback = PubkyRingAuthCallback.parse(url: url) {
guard isPaykitUIActive else {
app.toast(
type: .error,
title: t("profile__auth_error_title"),
description: t("other__qr_error_text")
)
return
}

let handlingResult = await pubkyProfile.handleAuthCallback(callback)

switch handlingResult {
Expand Down Expand Up @@ -368,7 +383,9 @@ struct MainNavView: View {

// Profile & Contacts
case .contacts:
if let initializationErrorMessage = pubkyProfile.initializationErrorMessage {
if !isPaykitUIActive {
ComingSoonScreen()
} else if let initializationErrorMessage = pubkyProfile.initializationErrorMessage {
pubkyInitializationErrorView(message: initializationErrorMessage)
} else if app.hasSeenContactsIntro || !contactsManager.contacts.isEmpty {
if !pubkyProfile.isInitialized {
Expand All @@ -383,12 +400,18 @@ struct MainNavView: View {
} else {
ContactsIntroView()
}
case .contactsIntro: ContactsIntroView()
case let .contactDetail(publicKey): ContactDetailView(publicKey: publicKey)
case let .contactActivity(publicKey): ContactActivityView(publicKey: publicKey)
case let .assignActivityContact(activityId): AssignActivityContactView(activityId: activityId)
case .contactsIntro:
if isPaykitUIActive { ContactsIntroView() } else { ComingSoonScreen() }
case let .contactDetail(publicKey):
if isPaykitUIActive { ContactDetailView(publicKey: publicKey) } else { paykitDisabledRedirectView }
case let .contactActivity(publicKey):
if isPaykitUIActive { ContactActivityView(publicKey: publicKey) } else { paykitDisabledRedirectView }
case let .assignActivityContact(activityId):
if isPaykitUIActive { AssignActivityContactView(activityId: activityId) } else { paykitDisabledRedirectView }
case .contactImportOverview:
if let fallbackRoute = fallbackRouteForMissingPendingImport(hasPendingImport: contactsManager.hasPendingImport) {
if !isPaykitUIActive {
paykitDisabledRedirectView
} else if let fallbackRoute = fallbackRouteForMissingPendingImport(hasPendingImport: contactsManager.hasPendingImport) {
missingPendingImportView(fallbackRoute: fallbackRoute)
} else if let profile = contactsManager.pendingImportProfile {
ContactImportOverviewView(
Expand All @@ -399,15 +422,21 @@ struct MainNavView: View {
missingPendingImportView(fallbackRoute: .payContacts)
}
case .contactImportSelect:
if let fallbackRoute = fallbackRouteForMissingPendingImport(hasPendingImport: contactsManager.hasPendingImport) {
if !isPaykitUIActive {
paykitDisabledRedirectView
} else if let fallbackRoute = fallbackRouteForMissingPendingImport(hasPendingImport: contactsManager.hasPendingImport) {
missingPendingImportView(fallbackRoute: fallbackRoute)
} else {
ContactImportSelectView(contacts: contactsManager.pendingImportContacts)
}
case let .addContact(publicKey): AddContactView(publicKey: publicKey)
case let .editContact(publicKey): EditContactView(publicKey: publicKey)
case let .addContact(publicKey):
if isPaykitUIActive { AddContactView(publicKey: publicKey) } else { paykitDisabledRedirectView }
case let .editContact(publicKey):
if isPaykitUIActive { EditContactView(publicKey: publicKey) } else { paykitDisabledRedirectView }
case .profile:
if let initializationErrorMessage = pubkyProfile.initializationErrorMessage {
if !isPaykitUIActive {
ComingSoonScreen()
} else if let initializationErrorMessage = pubkyProfile.initializationErrorMessage {
pubkyInitializationErrorView(message: initializationErrorMessage)
} else if !pubkyProfile.isInitialized {
pubkyLoadingView
Expand All @@ -418,11 +447,16 @@ struct MainNavView: View {
} else {
ProfileIntroView()
}
case .profileIntro: ProfileIntroView()
case .pubkyChoice: PubkyChoiceView()
case .createProfile: CreateProfileView()
case .editProfile: EditProfileView()
case .payContacts: PayContactsView()
case .profileIntro:
if isPaykitUIActive { ProfileIntroView() } else { ComingSoonScreen() }
case .pubkyChoice:
if isPaykitUIActive { PubkyChoiceView() } else { paykitDisabledRedirectView }
case .createProfile:
if isPaykitUIActive { CreateProfileView() } else { paykitDisabledRedirectView }
case .editProfile:
if isPaykitUIActive { EditProfileView() } else { paykitDisabledRedirectView }
case .payContacts:
if isPaykitUIActive { PayContactsView() } else { paykitDisabledRedirectView }

// Shop
case .shopIntro: ShopIntro()
Expand Down Expand Up @@ -451,7 +485,8 @@ struct MainNavView: View {
case .widgetsSettings: WidgetsSettingsScreen()
case .notifications: NotificationsSettings()
case .notificationsIntro: NotificationsIntro()
case .paymentPreference: PaymentPreferenceView()
case .paymentPreference:
if isPaykitUIActive { PaymentPreferenceView() } else { paykitDisabledRedirectView }

// Security settings
case .changePin: ChangePinScreen()
Expand Down Expand Up @@ -498,6 +533,13 @@ struct MainNavView: View {
}
}

private var paykitDisabledRedirectView: some View {
Color.customBlack
.task {
navigation.reset()
}
}

private func handleClipboard() {
Task { @MainActor in
guard let uri = UIPasteboard.general.string else {
Expand Down
Loading
Loading