Skip to content
Draft
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
19 changes: 9 additions & 10 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@
968FE13E2DFB016B0053CD7F /* XCRemoteSwiftPackageReference "ldk-node" */,
4AAB08C82E1FE77600BA63DF /* XCRemoteSwiftPackageReference "lottie-ios" */,
18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */,
182817BF2F59A7F10055A441 /* XCRemoteSwiftPackageReference "paykit-rs" */,
182817BF2F59A7F10055A441 /* XCLocalSwiftPackageReference "../paykit-rs" */,
);
productRefGroup = 96FE1F622C2DE6AA006D0C8B /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -1172,15 +1172,14 @@
};
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
182817BF2F59A7F10055A441 /* XCRemoteSwiftPackageReference "paykit-rs" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/pubky/paykit-rs";
requirement = {
kind = exactVersion;
version = "0.1.0-rc55";
};
/* Begin XCLocalSwiftPackageReference section */
182817BF2F59A7F10055A441 /* XCLocalSwiftPackageReference "../paykit-rs" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = "../paykit-rs";
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCRemoteSwiftPackageReference section */
18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/synonymdev/vss-rust-client-ffi";
Expand Down Expand Up @@ -1226,7 +1225,7 @@
/* Begin XCSwiftPackageProductDependency section */
182817C02F59A7F10055A441 /* Paykit */ = {
isa = XCSwiftPackageProductDependency;
package = 182817BF2F59A7F10055A441 /* XCRemoteSwiftPackageReference "paykit-rs" */;
package = 182817BF2F59A7F10055A441 /* XCLocalSwiftPackageReference "../paykit-rs" */;
productName = Paykit;
};
18D65DFF2EB964B500252335 /* VssRustClientFfi */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ struct AppScene: View {
@State private var hwWalletManager: HwWalletManager
@State private var calculatorInputManager = CalculatorInputManager()
@State private var paykitPaymentRequestManager = PaykitPaymentRequestManager()
@State private var paykitAllowanceManager = PaykitAllowanceManager()
@State private var initialPaykitSyncGeneration = 0

@State private var hideSplash = false
Expand Down Expand Up @@ -380,6 +381,7 @@ struct AppScene: View {
.environment(hwWalletManager)
.environment(calculatorInputManager)
.environment(paykitPaymentRequestManager)
.environment(paykitAllowanceManager)
}

private var appEventContent: some View {
Expand All @@ -388,6 +390,7 @@ struct AppScene: View {
if authState == .authenticated, let pk = pubkyProfile.publicKey {
paykitPaymentRequestManager.activate(identity: pk)
Task {
await paykitAllowanceManager.activate(identity: pk)
try? await contactsManager.loadContacts(for: pk)
await refreshPrivateOnlyPaykitReceiverMarker()
await refreshIncomingPaykitPaymentRequests(presentItems: false)
Expand All @@ -402,6 +405,7 @@ struct AppScene: View {
} else if authState == .idle {
contactsManager.reset()
paykitPaymentRequestManager.clear()
paykitAllowanceManager.deactivate()
}
}
.onReceive(contactsManager.$contacts) { contacts in
Expand All @@ -427,6 +431,9 @@ struct AppScene: View {
.onReceive(PaykitPaymentProofService.proofStateChangedPublisher) {
Task { await refreshIncomingPaykitPaymentRequests() }
}
.onReceive(PaykitAllowanceExecutor.eventPublisher.receive(on: DispatchQueue.main)) { event in
handlePaykitAllowanceEvent(event)
}
.onReceive(PaykitPaymentProofService.onchainPaymentResolutionPublisher) { resolution in
Task { await associateResolvedPaykitOnchainPayment(resolution) }
}
Expand Down Expand Up @@ -992,6 +999,10 @@ struct AppScene: View {
await PaykitPaymentProofService.shared.reconcile()
}
await paykitPaymentRequestManager.refresh()
await paykitAllowanceManager.refresh()
if await paykitAllowanceManager.processIncomingRequests(paykitPaymentRequestManager.pendingRequests) {
await paykitPaymentRequestManager.refresh()
}
if presentItems {
await presentNextIncomingPaykitItem()
}
Expand Down Expand Up @@ -1085,6 +1096,7 @@ struct AppScene: View {
guard sheets.activeSheetConfiguration == nil, !sheets.isReplacingSheet, app.contactPaymentContext == nil else { return }
for request in requests {
guard paykitPaymentRequestManager.isCurrentPresentation(request) else { return }
if await paykitAllowanceManager.isAutomaticallyHandling(request) { continue }
do {
let result = try await PrivatePaykitService.shared.beginPaymentRequest(request)
guard paykitPaymentRequestManager.isCurrentPresentation(request),
Expand Down Expand Up @@ -1300,6 +1312,44 @@ struct AppScene: View {
await presentNextIncomingPaykitPaymentRequest()
}

private func handlePaykitAllowanceEvent(_ event: PaykitAllowanceEvent) {
switch event {
case let .paidAutomatically(counterparty, amountSats, paymentId):
let title = t("subscriptions__allowance_executed_title")
let description = t(
"subscriptions__allowance_executed_description",
variables: ["amount": AllowanceAmountText.fiat(sats: amountSats, currency: currency), "name": contactName(counterparty)]
)
PaykitAllowanceNotifier.post(title: title, body: description, fallback: {
app.toast(type: .lightning, title: title, description: description, accessibilityIdentifier: "AllowancePaidToast")
})
Task {
await paykitAllowanceManager.refresh()
do {
try await activity.setContact(counterparty, forPaymentId: paymentId)
} catch {
Logger.warn("Failed to set contact for an automatic allowance payment: \(error)", context: "AppScene")
}
}
case let .limitReached(counterparty, amountSats):
let title = t("subscriptions__allowance_limit_title")
let description = t(
"subscriptions__allowance_limit_description",
variables: ["amount": AllowanceAmountText.fiat(sats: amountSats, currency: currency), "name": contactName(counterparty)]
)
PaykitAllowanceNotifier.post(title: title, body: description, fallback: {
app.toast(type: .warning, title: title, description: description, accessibilityIdentifier: "AllowanceLimitToast")
})
case .ledgerChanged:
Task { await paykitAllowanceManager.refresh() }
}
}

private func contactName(_ publicKey: String) -> String {
contactsManager.contacts.first { PubkyPublicKeyFormat.matches($0.publicKey, publicKey) }?.displayName
?? PubkyPublicKeyFormat.displayTruncated(publicKey)
}

private func presentNextIncomingPaykitItem() async {
guard sheets.activeSheetConfiguration == nil, !sheets.isReplacingSheet else { return }
if PaykitSubscriptionNotificationTargetStore.load() != nil {
Expand All @@ -1313,6 +1363,10 @@ struct AppScene: View {
sheets.showSheet(.subscription, data: SubscriptionSheetItem(route: .review(subscription)))
return
}
if let allowance = paykitAllowanceManager.proposalForPresentation() {
sheets.showSheet(.subscription, data: SubscriptionSheetItem(route: .allowanceReview(allowance)))
return
}
await presentNextIncomingPaykitPaymentRequest()
}

Expand Down
47 changes: 47 additions & 0 deletions Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1749,3 +1749,50 @@
"settings__adv__pp_both" = "Both";
"settings__adv__pp_lightning_short" = "Lightning";
"settings__adv__pp_onchain_short" = "On-chain";
"subscriptions__allowances" = "Allowances";
"subscriptions__allowances_empty_headline" = "Set up\n<accent>allowances</accent>";
"subscriptions__allowances_empty_description" = "You can set spending limits to automatically fulfill payment requests from trusted peers.";
"subscriptions__allowance_add" = "Add Allowance";
"subscriptions__allowance_title" = "Allowance";
"subscriptions__allowance_choose_contact" = "Choose Contact";
"subscriptions__allowance_no_contacts" = "Add a contact first. Allowances cover payment requests from your contacts.";
"subscriptions__allowance_set_title" = "Set Allowance";
"subscriptions__allowance_set_explanation" = "Automatically approve payment requests from {name} below these limits:";
"subscriptions__allowance_payment_limit" = "Payment limit";
"subscriptions__allowance_monthly_allowance" = "Monthly allowance";
"subscriptions__allowance_set_summary" = "Requests up to {perPayment} will be paid automatically, up to {monthly} a month, without asking you each time.";
"subscriptions__allowance_save" = "Save Allowance";
"subscriptions__allowance_monthly_limit" = "Monthly limit";
"subscriptions__allowance_per_payment_short" = "{amount} a payment";
"subscriptions__allowance_per_payment" = "Per payment";
"subscriptions__allowance_each_month" = "Each month";
"subscriptions__allowance_up_to" = "Up to {amount}";
"subscriptions__allowance_status_active" = "Active";
"subscriptions__allowance_status_waiting" = "Waiting for an answer";
"subscriptions__allowance_status_needs_answer" = "Waiting for your answer";
"subscriptions__allowance_status_scheduled" = "Not active yet";
"subscriptions__allowance_status_expired" = "Expired";
"subscriptions__allowance_status_declined" = "Declined";
"subscriptions__allowance_status_conflicted" = "Needs attention";
"subscriptions__allowance_status_ended" = "Ended";
"subscriptions__allowance_paid_automatically" = "{amount} paid automatically";
"subscriptions__allowance_paid_so_far" = "Paid automatically";
"subscriptions__allowance_offer_headline" = "{name} offers\n<accent>an allowance</accent>";
"subscriptions__allowance_request_headline" = "{name} asks for\n<accent>an allowance</accent>";
"subscriptions__allowance_offer_explanation" = "{name}'s wallet will pay your requests within these limits without asking each time. Either of you can end it.";
"subscriptions__allowance_request_explanation" = "Your wallet will pay {name}'s requests within these limits without asking you each time. Either of you can end it.";
"subscriptions__allowance_accept" = "Accept";
"subscriptions__allowance_decline" = "Decline";
"subscriptions__allowance_swipe_end" = "Swipe To End Allowance";
"subscriptions__allowance_swipe_withdraw" = "Swipe To Withdraw";
"subscriptions__allowance_detail_active_allower" = "Requests within these limits are paid automatically. Anything above them asks you first.";
"subscriptions__allowance_detail_active_allowee" = "Your requests within these limits are paid automatically.";
"subscriptions__allowance_detail_ended" = "This allowance has ended. Every request asks again.";
"subscriptions__allowance_error_not_linked" = "This contact is not connected yet. Try again in a moment.";
"subscriptions__allowance_error_unavailable" = "This allowance is not available right now.";
"subscriptions__allowance_payment_in_progress" = "This request is already being paid.";
"subscriptions__allowance_executed_title" = "Payment Executed";
"subscriptions__allowance_executed_description" = "Auto-pay sent {amount} to {name}";
"subscriptions__allowance_limit_title" = "Limit Reached";
"subscriptions__allowance_limit_description" = "A {amount} request from {name} is above your allowance. Review it to pay.";
"subscriptions__allowance_auto_paid" = "Auto-paid";
Loading