Skip to content
Open
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
112 changes: 107 additions & 5 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ struct AppScene: View {
}

var body: some View {
appEventContent
}

private var configuredContent: some View {
mainContent
.sheet(
item: $sheets.forgotPinSheetItem,
Expand All @@ -141,6 +145,7 @@ struct AppScene: View {
.task(priority: .userInitiated, setupTask)
.task(id: scenePhase) { await pollIncomingPaykitPaymentRequests() }
.task(id: initialPaykitSyncGeneration) { await pollIncomingPaykitPaymentRequestsDuringInitialSync() }
.task { await handlePendingPaykitSubscriptionNotification() }
.onChange(of: currency.hasStaleData) { _, newValue in handleCurrencyStaleData(newValue) }
.onChange(of: wallet.walletExists) { _, newValue in handleWalletExistsChange(newValue) }
.onChange(of: wallet.nodeLifecycleState) { _, newValue in handleNodeLifecycleChange(newValue) }
Expand Down Expand Up @@ -200,13 +205,21 @@ struct AppScene: View {
.environment(hwWalletManager)
.environment(calculatorInputManager)
.environment(paykitPaymentRequestManager)
}

private var appEventContent: some View {
configuredContent
.onChange(of: pubkyProfile.authState, initial: true) { _, authState in
if authState == .authenticated, let pk = pubkyProfile.publicKey {
paykitPaymentRequestManager.activate(identity: pk)
Task {
try? await contactsManager.loadContacts(for: pk)
await refreshPrivateOnlyPaykitReceiverMarker()
await refreshIncomingPaykitPaymentRequests()
await refreshIncomingPaykitPaymentRequests(presentItems: false)
await handlePendingPaykitSubscriptionNotification()
if PaykitSubscriptionNotificationTargetStore.load() == nil {
await presentNextIncomingPaykitItem()
}
if !PaykitFeatureFlags.isUIEnabled, wallet.walletExists == true {
await retryPendingPaykitEndpointRemoval()
}
Expand Down Expand Up @@ -236,6 +249,15 @@ struct AppScene: View {
.onReceive(PrivatePaykitService.initialLinkBurstStartedPublisher) {
initialPaykitSyncGeneration += 1
}
.onReceive(PaykitPaymentProofService.proofStateChangedPublisher) {
Task { await refreshIncomingPaykitPaymentRequests() }
}
.onReceive(PaykitPaymentProofService.onchainPaymentResolutionPublisher) { resolution in
Task { await associateResolvedPaykitOnchainPayment(resolution) }
}
.onReceive(NotificationCenter.default.publisher(for: .paykitSubscriptionPaymentDue)) { _ in
Task { await handlePendingPaykitSubscriptionNotification() }
}
.onChange(of: sheets.activeSheetConfiguration?.id) { _, activeSheetId in
guard activeSheetId == nil, !sheets.isReplacingSheet else { return }
Task {
Expand All @@ -244,7 +266,7 @@ struct AppScene: View {
sheets.activeSheetConfiguration == nil,
!sheets.isReplacingSheet
else { return }
await presentNextIncomingPaykitPaymentRequest()
await presentNextIncomingPaykitItem()
}
}
.onChange(of: paykitPaymentRequestManager.requestedPresentationId) { _, requestId in
Expand Down Expand Up @@ -761,7 +783,7 @@ struct AppScene: View {
}

@discardableResult
private func refreshIncomingPaykitPaymentRequests() async -> Bool {
private func refreshIncomingPaykitPaymentRequests(presentItems: Bool = true) async -> Bool {
guard PaykitFeatureFlags.isUIEnabled,
wallet.walletExists == true,
pubkyProfile.authState == .authenticated
Expand All @@ -774,10 +796,39 @@ struct AppScene: View {
let previousRequests = paykitPaymentRequestManager.pendingRequests
await paykitPaymentRequestManager.refreshEligibleTargets(savedPublicKeys: contactsManager.contacts.map(\.publicKey))
await paykitPaymentRequestManager.refresh()
await presentNextIncomingPaykitPaymentRequest()
if presentItems {
await presentNextIncomingPaykitItem()
}
return paykitPaymentRequestManager.pendingRequests != previousRequests
}

private func associateResolvedPaykitOnchainPayment(_ resolution: PaykitOnchainPaymentResolution) async {
guard let identity = pubkyProfile.publicKey,
PubkyPublicKeyFormat.matches(resolution.identity, identity)
else { return }
do {
_ = try await tryNTimes(
toTry: {
try? await activity.syncLdkNodePayments()
return try await activity.findActivity(byPaymentId: resolution.transactionId)
},
times: 12,
interval: 2
)
try await activity.setContact(
resolution.requestId.counterparty,
forPaymentId: resolution.transactionId,
syncLdkPayments: false
)
await PaykitPaymentProofService.shared.consumeOnchainPaymentResolution(resolution)
} catch {
Logger.warn(
"Failed to associate resolved Paykit payment \(resolution.transactionId) with its contact: \(error)",
context: "AppScene"
)
}
}

private func pollIncomingPaykitPaymentRequests() async {
guard scenePhase == .active else { return }

Expand Down Expand Up @@ -842,7 +893,8 @@ struct AppScene: View {
let contactPaymentContext = ContactPaymentContext(
publicKey: request.counterparty,
privatePaymentContext: privatePaymentContext,
incomingPaymentRequest: request
incomingPaymentRequest: request,
isInitialSubscriptionPayment: paykitPaymentRequestManager.consumeInitialSubscriptionPayment(request)
)
guard app.claimContactPaymentContext(contactPaymentContext) else { return }

Expand Down Expand Up @@ -922,6 +974,56 @@ struct AppScene: View {
await presentNextIncomingPaykitPaymentRequest()
}

private func handlePendingPaykitSubscriptionNotification() async {
guard let target = PaykitSubscriptionNotificationTargetStore.load() else { return }
guard let identity = pubkyProfile.publicKey else { return }
guard target.matches(identity: identity) else {
PaykitSubscriptionNotificationTargetStore.clear()
return
}
guard sheets.activeSheetConfiguration == nil,
!sheets.isReplacingSheet,
app.contactPaymentContext == nil
else { return }
await refreshIncomingPaykitPaymentRequests(presentItems: false)
guard let request = paykitPaymentRequestManager.pendingRequests.first(where: target.matches) else {
if paykitPaymentRequestManager.historyRequests.contains(where: target.matches) {
PaykitSubscriptionNotificationTargetStore.clear()
} else if paykitPaymentRequestManager.hasDismissedSubscriptionPayment(matching: target) {
PaykitSubscriptionNotificationTargetStore.clear()
} else if !paykitPaymentRequestManager.subscriptions.contains(where: {
$0.paymentRequestId == target.paymentRequestId &&
PubkyPublicKeyFormat.matches($0.counterparty, target.counterparty) &&
$0.counterpartyReceiverPath == target.counterpartyReceiverPath &&
$0.isActive(at: Date())
}) {
PaykitSubscriptionNotificationTargetStore.clear()
}
return
}

if paykitPaymentRequestManager.requestedPresentationId != request.id {
guard paykitPaymentRequestManager.requestPresentation(request) else { return }
}
await presentNextIncomingPaykitPaymentRequest()
}

private func presentNextIncomingPaykitItem() async {
guard sheets.activeSheetConfiguration == nil, !sheets.isReplacingSheet else { return }
if PaykitSubscriptionNotificationTargetStore.load() != nil {
await handlePendingPaykitSubscriptionNotification()
guard PaykitSubscriptionNotificationTargetStore.load() == nil,
sheets.activeSheetConfiguration == nil,
!sheets.isReplacingSheet
else { return }
}
if let subscription = paykitPaymentRequestManager.subscriptionProposalForPresentation() {
sheets.showSheet(.subscription, data: SubscriptionSheetItem(route: .review(subscription)))
return
}
await presentNextIncomingPaykitPaymentRequest()
}

private func retryPendingPaykitEndpointRemoval() async {
if PublicPaykitService.isCleanupPending {
do {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "subscription-clock.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 72 additions & 1 deletion Bitkit/BitkitApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,70 @@ import SwiftUI
/// Communication bridge between delegates and SwiftUI views
extension Notification.Name {
static let quickActionSelected = Notification.Name("quickActionSelected")
static let paykitSubscriptionPaymentDue = Notification.Name("paykitSubscriptionPaymentDue")
}

struct PaykitSubscriptionNotificationTarget: Codable, Equatable {
let payerIdentity: String
let paymentRequestId: String
let counterparty: String
let counterpartyReceiverPath: String
let billingPeriodStartsAt: String

init?(userInfo: [AnyHashable: Any]) {
guard let payerIdentity = userInfo["payer_identity"] as? String,
let paymentRequestId = userInfo["payment_request_id"] as? String,
let counterparty = userInfo["counterparty"] as? String,
let counterpartyReceiverPath = userInfo["counterparty_receiver_path"] as? String,
let billingPeriodStartsAt = userInfo["billing_period_starts_at"] as? String
else { return nil }

self.payerIdentity = payerIdentity
self.paymentRequestId = paymentRequestId
self.counterparty = counterparty
self.counterpartyReceiverPath = counterpartyReceiverPath
self.billingPeriodStartsAt = billingPeriodStartsAt
}

func matches(_ request: PaykitPaymentRequest) -> Bool {
paymentRequestId == request.paymentRequestId &&
PubkyPublicKeyFormat.matches(counterparty, request.counterparty) &&
counterpartyReceiverPath == request.counterpartyReceiverPath &&
request.billingPeriod.map {
PaykitSubscriptionTimestamp.string(from: $0.startsAt) == billingPeriodStartsAt
} == true
}

func matches(_ requestId: PaykitPaymentRequest.ID) -> Bool {
paymentRequestId == requestId.paymentRequestId &&
PubkyPublicKeyFormat.matches(counterparty, requestId.counterparty) &&
counterpartyReceiverPath == requestId.counterpartyReceiverPath &&
requestId.billingPeriodStartsAt.map {
PaykitSubscriptionTimestamp.string(from: $0) == billingPeriodStartsAt
} == true
}

func matches(identity: String) -> Bool {
PubkyPublicKeyFormat.matches(payerIdentity, identity)
}
}

enum PaykitSubscriptionNotificationTargetStore {
private static let key = "paykitSubscriptionNotificationTarget"

static func save(_ target: PaykitSubscriptionNotificationTarget) {
guard let data = try? JSONEncoder().encode(target) else { return }
UserDefaults.standard.set(data, forKey: key)
}

static func load() -> PaykitSubscriptionNotificationTarget? {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
return try? JSONDecoder().decode(PaykitSubscriptionNotificationTarget.self, from: data)
}

static func clear() {
UserDefaults.standard.removeObject(forKey: key)
}
}

class AppDelegate: NSObject, UIApplicationDelegate {
Expand Down Expand Up @@ -81,7 +145,14 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
) {
let userInfo = response.notification.request.content.userInfo

PushNotificationManager.shared.handleNotification(userInfo)
if userInfo["bitkit_action"] as? String == "paykit_subscription_due" {
if let target = PaykitSubscriptionNotificationTarget(userInfo: userInfo) {
PaykitSubscriptionNotificationTargetStore.save(target)
}
NotificationCenter.default.post(name: .paykitSubscriptionPaymentDue, object: nil, userInfo: userInfo)
} else {
PushNotificationManager.shared.handleNotification(userInfo)
}

// TODO: if user tapped on an incoming tx we should open it on that tx view
completionHandler()
Expand Down
3 changes: 2 additions & 1 deletion Bitkit/Components/Button/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ struct CustomButton: View {
icon: icon,
isDisabled: effectiveIsDisabled,
isPressed: isPressed,
isLoading: isLoading
isLoading: isLoading,
shouldExpand: shouldExpand
))
case .tertiary:
AnyView(TertiaryButtonView(
Expand Down
18 changes: 7 additions & 11 deletions Bitkit/Components/Button/SecondaryButtonView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ struct SecondaryButtonView: View {
let isDisabled: Bool
let isPressed: Bool
var isLoading: Bool = false
let shouldExpand: Bool

var body: some View {
HStack(spacing: 8) {
Expand All @@ -24,8 +25,8 @@ struct SecondaryButtonView: View {
BodySSBText(title, textColor: textColor)
}
}
.frame(maxWidth: size == .large ? .infinity : nil)
.frame(height: buttonHeight)
.frame(maxWidth: (size == .large || shouldExpand) ? .infinity : nil)
.frame(height: size.height)
.padding(.horizontal, 16)
.background(isPressed ? Color.white10 : Color.clear)
.background(BlurView())
Expand All @@ -35,18 +36,13 @@ struct SecondaryButtonView: View {
}

private var textColor: Color {
isDisabled ? .white32 : .white80
guard !isDisabled else { return .white32 }
return size == .small ? .white64 : .white80
}

private var borderColor: Color {
isDisabled ? .clear : .gray4
}

private var buttonHeight: CGFloat {
switch size {
case .small: 37
case .large: 56
}
guard !isDisabled else { return .clear }
return size == .small ? .white16 : .gray4
}

private var strokeWidth: CGFloat {
Expand Down
Loading
Loading