diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 327f85c10..d1c1db580 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -1178,7 +1178,7 @@ repositoryURL = "https://github.com/pubky/paykit-rs"; requirement = { kind = exactVersion; - version = "0.1.0-rc55"; + version = "0.1.0-rc56"; }; }; 18D65DFE2EB9649F00252335 /* XCRemoteSwiftPackageReference "vss-rust-client-ffi" */ = { diff --git a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 81b2ef402..0d91d3e52 100644 --- a/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Bitkit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "746cacd44d171f1c992b592ea1224d0562117bffd2907a54dee9d441395ac6b4", + "originHash" : "86914da7e92df3dff5ecfc5ee3ce34ea4b3f911278d0c81ac20b137bacf28367", "pins" : [ { "identity" : "bitkit-core", @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pubky/paykit-rs", "state" : { - "revision" : "4613beee25d1680ed8bc849446498e9bfb2cdb16", - "version" : "0.1.0-rc55" + "revision" : "24162ebbcc703251d8038f2e176d1f4cfb117c6a", + "version" : "0.1.0-rc56" } }, { diff --git a/Bitkit/Services/PaykitPaymentProofService.swift b/Bitkit/Services/PaykitPaymentProofService.swift index 9a42330b4..a2ec54aa9 100644 --- a/Bitkit/Services/PaykitPaymentProofService.swift +++ b/Bitkit/Services/PaykitPaymentProofService.swift @@ -688,6 +688,8 @@ actor PaykitPaymentProofService { proof: Paykit.PaymentProofSubmission( billingPeriod: pendingProof.billingPeriod?.sdkValue, paymentEndpointIdentifier: pendingProof.paymentEndpointIdentifier, + allowanceId: nil, + conversionQuoteId: nil, proof: Paykit.PrivateJsonObject(text: proofText) ) ) diff --git a/Bitkit/Services/PaykitPaymentRequestService.swift b/Bitkit/Services/PaykitPaymentRequestService.swift index ecba43b75..780f26a99 100644 --- a/Bitkit/Services/PaykitPaymentRequestService.swift +++ b/Bitkit/Services/PaykitPaymentRequestService.swift @@ -13,6 +13,7 @@ struct PaykitPaymentRequest: Identifiable, Hashable { case missingTerms = "missing_terms" case recurringRequest = "recurring_request" case unsupportedAsset = "unsupported_asset" + case unsupportedPaymentDeadline = "unsupported_payment_deadline" case invalidAmount = "invalid_amount" case amountOutOfRange = "amount_out_of_range" case noSupportedEndpoint = "no_supported_endpoint" @@ -133,6 +134,9 @@ struct PaykitPaymentRequest: Identifiable, Hashable { guard record.state != .activeRecurring else { return .failure(.recurringRequest) } guard let terms = record.terms else { return .failure(.missingTerms) } guard terms.recurrence == nil else { return .failure(.recurringRequest) } + if requiresActionableRequest, terms.paymentDeadline != nil { + return .failure(.unsupportedPaymentDeadline) + } guard terms.amount.asset == PaykitIssuerInterop.bitcoinAsset else { return .failure(.unsupportedAsset) } guard let amountSats = Self.sats(fromBitcoinAmount: terms.amount.value) else { return .failure(.invalidAmount) } guard amountSats <= UInt64.max / 1000 else { return .failure(.amountOutOfRange) } @@ -666,6 +670,8 @@ struct PaykitPaymentRequestService { proposalExpiresAt: Self.timestamp(draft.expiresAt), recurrence: nil, acceptedPaymentEndpointIdentifiers: acceptedPaymentEndpointIdentifiers, + conversion: nil, + paymentDeadline: nil, metadata: Paykit.PrivateJsonObject(text: metadataText) ) let record = try await sdk.proposePaymentRequest( @@ -795,6 +801,8 @@ struct PaykitPaymentRequestService { proposalExpiresAt: Self.timestamp(draft.expiresAt), recurrence: recurrence, acceptedPaymentEndpointIdentifiers: endpoints, + conversion: nil, + paymentDeadline: nil, metadata: Paykit.PrivateJsonObject(text: metadataText) ) } @@ -1937,7 +1945,7 @@ final class PaykitPaymentRequestManager { dismissedSubscriptionPaymentIds.formIntersection(activeRecurringRequestIds) persistSubscriptionState() let recurringPending = recurringRequestsBySubscription - .filter { $0.0.lifecycleState == .activeRecurring } + .filter { $0.0.lifecycleState == .activeRecurring && !$0.0.hasPaymentDeadline } .flatMap { _, requests in requests.filter { $0.lifecycleState != .proofSubmitted && @@ -2033,7 +2041,7 @@ final class PaykitPaymentRequestManager { subscription.requests(through: date, acceptedAt: $0) } ?? [] pendingRequests.removeAll { $0.belongs(to: subscription) } - if subscription.lifecycleState == .activeRecurring { + if subscription.lifecycleState == .activeRecurring, !subscription.hasPaymentDeadline { pendingRequests.append(contentsOf: recurringRequests.filter { $0.lifecycleState != .proofSubmitted }) pendingRequests.sort { ($0.createdAt ?? .distantFuture) < ($1.createdAt ?? .distantFuture) } } diff --git a/Bitkit/Services/PaykitSubscription.swift b/Bitkit/Services/PaykitSubscription.swift index 5ca678875..11cb4bafa 100644 --- a/Bitkit/Services/PaykitSubscription.swift +++ b/Bitkit/Services/PaykitSubscription.swift @@ -440,6 +440,7 @@ struct PaykitSubscription: Identifiable, Hashable { let note: String? let createdAt: Date? let proposalExpiresAt: Date? + let hasPaymentDeadline: Bool let recurrence: PaykitSubscriptionRecurrence let metadata: PaykitSubscriptionMetadata let acceptedPaymentEndpointIdentifiers: [String] @@ -475,6 +476,7 @@ struct PaykitSubscription: Identifiable, Hashable { func isProposalActionable(at date: Date) -> Bool { isProposalVisible(at: date) && + !hasPaymentDeadline && recurrence.unit.isSupported && recurrence.canMaterializePeriods && !acceptedPaymentEndpointIdentifiers.isEmpty @@ -555,6 +557,7 @@ struct PaykitSubscription: Identifiable, Hashable { note = PaykitPaymentRequest.note(from: terms.metadata).map { String($0.prefix(256)) } createdAt = record.lastEventAt.flatMap(PaykitPaymentRequest.parseDate) self.proposalExpiresAt = proposalExpiresAt + hasPaymentDeadline = terms.paymentDeadline != nil self.recurrence = recurrence metadata = PaykitSubscriptionMetadata(terms.metadata) acceptedPaymentEndpointIdentifiers = PaykitIssuerInterop.supportedEndpointIdentifiers( @@ -594,7 +597,7 @@ struct PaykitSubscription: Identifiable, Hashable { } func paymentDueOnAcceptance(at date: Date) -> PaykitPaymentRequest? { - guard isPayer else { return nil } + guard isPayer, !hasPaymentDeadline else { return nil } guard let period = recurrence.periods(through: date, acceptedAt: PaykitPreciseInstant(date: date)).first else { return nil } return PaykitPaymentRequest(subscription: self, billingPeriod: period, lifecycleState: .activeRecurring) } @@ -719,6 +722,7 @@ actor PaykitSubscriptionNotificationScheduler { .filter { $0.isPayer && $0.isActive(at: now) && + !$0.hasPaymentDeadline && $0.recurrence.unit.isSupported && acceptedAt[$0.id] != nil } diff --git a/Bitkit/Views/Subscriptions/SubscriptionsView.swift b/Bitkit/Views/Subscriptions/SubscriptionsView.swift index 40eef3db1..9668e750a 100644 --- a/Bitkit/Views/Subscriptions/SubscriptionsView.swift +++ b/Bitkit/Views/Subscriptions/SubscriptionsView.swift @@ -657,7 +657,7 @@ struct SubscriptionSheet: View { BodyMText(t("subscriptions__unsupported_description"), textColor: .white64) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 16) - } else if subscription.acceptedPaymentEndpointIdentifiers.isEmpty { + } else if subscription.hasPaymentDeadline || subscription.acceptedPaymentEndpointIdentifiers.isEmpty { BodyMText(t("subscriptions__unsupported_payment_description"), textColor: .white64) .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 16) diff --git a/BitkitTests/PaykitIssuerInteropTests.swift b/BitkitTests/PaykitIssuerInteropTests.swift index a5a018f91..f04ed035e 100644 --- a/BitkitTests/PaykitIssuerInteropTests.swift +++ b/BitkitTests/PaykitIssuerInteropTests.swift @@ -102,6 +102,8 @@ final class PaykitIssuerInteropTests: XCTestCase { proposalExpiresAt: nil, recurrence: nil, acceptedPaymentEndpointIdentifiers: endpointIdentifiers, + conversion: nil, + paymentDeadline: nil, metadata: PrivateJsonObject(text: #"{"order":"713"}"#) ), acceptedEventId: nil, @@ -110,6 +112,7 @@ final class PaykitIssuerInteropTests: XCTestCase { rejectedOutboundStatus: nil, canceledEventId: nil, canceledOutboundStatus: nil, + conversionQuotes: [], paymentProofs: [], lastStreamItemId: 1, lastOutboundMessageId: nil, diff --git a/BitkitTests/PaykitPaymentProofServiceTests.swift b/BitkitTests/PaykitPaymentProofServiceTests.swift index 6d30e47a4..b7b7ddd4a 100644 --- a/BitkitTests/PaykitPaymentProofServiceTests.swift +++ b/BitkitTests/PaykitPaymentProofServiceTests.swift @@ -1229,6 +1229,8 @@ final class PaykitPaymentProofServiceTests: XCTestCase { proposalExpiresAt: nil, recurrence: recurrence, acceptedPaymentEndpointIdentifiers: endpoints, + conversion: nil, + paymentDeadline: nil, metadata: PrivateJsonObject(text: "{}") ), acceptedEventId: nil, @@ -1237,6 +1239,7 @@ final class PaykitPaymentProofServiceTests: XCTestCase { rejectedOutboundStatus: nil, canceledEventId: nil, canceledOutboundStatus: nil, + conversionQuotes: [], paymentProofs: paymentProofs, lastStreamItemId: 1, lastOutboundMessageId: nil, @@ -1260,6 +1263,8 @@ final class PaykitPaymentProofServiceTests: XCTestCase { paymentReference: PaymentReference(text: "invoice-123"), billingPeriod: billingPeriod, paymentEndpointIdentifier: endpoint, + allowanceId: nil, + conversionQuoteId: nil, proof: PrivateJsonObject(text: "{\"data\":\"\(data)\",\"type\":\"\(kind.rawValue)\"}"), recordedAt: "2027-01-15T08:01:00Z" ) @@ -1442,6 +1447,8 @@ private actor PaymentProofSdkMock: PaykitPaymentProofSdkHandling { paymentReference: paymentReference, billingPeriod: proof.billingPeriod, paymentEndpointIdentifier: proof.paymentEndpointIdentifier, + allowanceId: nil, + conversionQuoteId: nil, proof: proof.proof, recordedAt: "2027-01-15T08:01:00Z" )) diff --git a/BitkitTests/PaykitPaymentRequestServiceTests.swift b/BitkitTests/PaykitPaymentRequestServiceTests.swift index 97e863a9f..b764843ff 100644 --- a/BitkitTests/PaykitPaymentRequestServiceTests.swift +++ b/BitkitTests/PaykitPaymentRequestServiceTests.swift @@ -234,6 +234,10 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { (paymentRequestRecord(id: "unknown-role", role: .unknown), .unsupportedLocalRole), (paymentRequestRecord(id: "missing-terms"), .missingTerms), (paymentRequestRecord(id: "wrong-asset", asset: "BTC"), .unsupportedAsset), + ( + paymentRequestRecord(id: "payment-deadline", paymentDeadline: .at(timestamp: timestamp(now.addingTimeInterval(3600)))), + .unsupportedPaymentDeadline + ), (paymentRequestRecord(id: "invalid-amount", amount: "not-bitcoin"), .invalidAmount), (paymentRequestRecord(id: "amount-out-of-range", amount: "184467440737.09551615"), .amountOutOfRange), (paymentRequestRecord(id: "unsupported-endpoint", endpoints: ["btc-unsupported-method"]), .noSupportedEndpoint), @@ -360,6 +364,11 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { paymentRequestRecord(id: "outgoing", state: .proposed, role: .payee), paymentRequestRecord(id: "recurring", state: .activeRecurring), paymentRequestRecord(id: "unsupported", state: .canceled, endpoints: ["btc-unsupported-method"]), + paymentRequestRecord(id: "deadline-proposed", paymentDeadline: .at(timestamp: timestamp(now))), + paymentRequestRecord(id: "deadline-accepted", state: .accepted, paymentDeadline: .at(timestamp: timestamp(now))), + paymentRequestRecord(id: "deadline-paid", state: .proofSubmitted, paymentDeadline: .at(timestamp: timestamp(now))), + paymentRequestRecord(id: "deadline-canceled", state: .canceled, paymentDeadline: .at(timestamp: timestamp(now))), + paymentRequestRecord(id: "deadline-rejected", state: .rejected, paymentDeadline: .at(timestamp: timestamp(now))), ] let manager = paymentRequestManager( sdk: PaymentRequestSdkMock(records: records), @@ -371,7 +380,10 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { XCTAssertEqual(manager.pendingRequests.map(\.paymentRequestId), ["incoming", "accepted"]) XCTAssertEqual( Set(manager.historyRequests.map(\.paymentRequestId)), - Set(["incoming", "accepted", "rejected", "expired", "outgoing", "unsupported"]) + Set([ + "incoming", "accepted", "rejected", "expired", "outgoing", "unsupported", + "deadline-proposed", "deadline-accepted", "deadline-paid", "deadline-canceled", "deadline-rejected", + ]) ) XCTAssertEqual( manager.historyRequests.first { $0.paymentRequestId == "accepted" }?.lifecycleState, @@ -1063,7 +1075,9 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { anchor: "2027-01-01T08:00:00Z", endsAt: nil ) - let record = try paymentRequestRecord(state: .activeRecurring, recurrence: recurrence) + let record = try paymentRequestRecord( + state: .activeRecurring, paymentDeadline: .periodStart(seconds: 3600), recurrence: recurrence + ) let subscription = try XCTUnwrap(PaykitSubscription(record: record)) let request = try XCTUnwrap(subscription.requests(through: now, acceptedAt: PaykitPreciseInstant(date: now)).first) let manager = paymentRequestManager( @@ -1121,6 +1135,51 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { } } + func testDeadlineSubscriptionsKeepPaidPeriodsAndCancellationWithoutOfferingPayments() async throws { + let now = try XCTUnwrap(ISO8601DateFormatter().date(from: "2027-02-15T08:00:00Z")) + let recurrence = PaymentRequestRecurrence( + every: 1, unit: "month", startsAt: "2027-01-01T08:00:00Z", anchor: "2027-01-01T08:00:00Z", endsAt: nil + ) + let proof = try paymentProofRecord( + endpoint: PublicPaykitService.MethodId.bitcoinLightningBolt11.rawValue, + kind: .lightning, + billingPeriod: BillingPeriod(startsAt: "2027-01-01T08:00:00Z", endsAt: "2027-02-01T08:00:00Z") + ) + let records = try [PaymentRequestLocalRole.payer, .payee].map { (role: PaymentRequestLocalRole) in + try paymentRequestRecord( + id: "deadline-\(role)", state: .activeRecurring, role: role, + paymentDeadline: .periodStart(seconds: 3600), recurrence: recurrence, paymentProofs: [proof] + ) + } + let center = PaykitSubscriptionNotificationCenterMock() + let scheduler = PaykitSubscriptionNotificationScheduler(center: center) + let manager = paymentRequestManager( + sdk: PaymentRequestSdkMock(records: records), clock: PaymentRequestTestClock(now) + ) + + await manager.refresh() + + XCTAssertTrue(manager.pendingRequests.isEmpty) + XCTAssertEqual(manager.subscriptions.count, 2) + for subscription in manager.subscriptions { + XCTAssertEqual(subscription.paidPeriods.count, 1) + XCTAssertTrue(subscription.canCancel(at: now)) + XCTAssertNil(subscription.paymentDueOnAcceptance(at: now)) + } + let paid = try XCTUnwrap(manager.historyRequests.first) + XCTAssertEqual(manager.historyRequests.count, 1) + XCTAssertEqual(paid.lifecycleState, .proofSubmitted) + XCTAssertEqual(paid.paymentProofKind, .lightning) + XCTAssertEqual(manager.subscriptions.first { $0.isCreatedByUser }?.receivedPaymentRequests().count, 1) + await scheduler.synchronize( + manager.subscriptions, + acceptedAt: Dictionary(uniqueKeysWithValues: manager.subscriptions.map { ($0.id, PaykitPreciseInstant(date: now)) }), + pendingRequestIds: [], payerIdentity: "payer", notificationsEnabled: true, now: now + ) + let pendingNotifications = await center.pendingIdentifiers + XCTAssertTrue(pendingNotifications.isEmpty) + } + func testInFlightSubscriptionPaymentIsNotOfferedOrMarkedPaid() async throws { let now = try XCTUnwrap(ISO8601DateFormatter().date(from: "2027-01-15T08:00:00Z")) let recurrence = PaymentRequestRecurrence( @@ -1591,6 +1650,7 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { let manager = try paymentRequestManager( sdk: PaymentRequestSdkMock(records: [ paymentRequestRecord(id: "malformed", expiresAt: "not-a-timestamp", recurrence: recurrence), + paymentRequestRecord(id: "deadline", paymentDeadline: .periodStart(seconds: 3600), recurrence: recurrence), paymentRequestRecord(id: "unsupported", recurrence: recurrence, endpoints: ["btc-unsupported-method"]), paymentRequestRecord(id: "ended", recurrence: endedRecurrence), ]), @@ -1599,10 +1659,18 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { await manager.refresh() - let subscription = try XCTUnwrap(manager.subscriptions.first) - XCTAssertEqual(subscription.paymentRequestId, "unsupported") - XCTAssertFalse(subscription.isProposalActionable(at: Date(timeIntervalSince1970: 1_800_000_000))) - XCTAssertEqual(manager.subscriptionProposalForPresentation()?.id, subscription.id) + let deadlineSubscription = try XCTUnwrap(manager.subscriptions.first { $0.paymentRequestId == "deadline" }) + let subscription = try XCTUnwrap(manager.subscriptions.first { $0.paymentRequestId == "unsupported" }) + XCTAssertFalse(subscription.isProposalActionable(at: expiration)) + XCTAssertFalse(deadlineSubscription.isProposalActionable(at: expiration)) + XCTAssertNil(deadlineSubscription.paymentDueOnAcceptance(at: expiration)) + XCTAssertEqual(manager.subscriptionProposalForPresentation()?.id, deadlineSubscription.id) + do { + _ = try await manager.accept(deadlineSubscription) + XCTFail("Unsupported payment terms must not be accepted") + } catch { + XCTAssertEqual(error as? PaykitPaymentRequestError, .requestUnavailable) + } XCTAssertEqual( manager.subscriptions.first { $0.paymentRequestId == "ended" }?.lifecycleState, .proposalExpired @@ -3639,6 +3707,7 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { amount: String = "0.001", asset: String = "btc", expiresAt: String? = nil, + paymentDeadline: PaymentDeadline? = nil, recurrence: PaymentRequestRecurrence? = nil, endpoints: [String] = [PublicPaykitService.MethodId.bitcoinLightningBolt11.rawValue], metadata: String = "{}", @@ -3664,6 +3733,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { proposalExpiresAt: expiresAt, recurrence: recurrence, acceptedPaymentEndpointIdentifiers: endpoints, + conversion: nil, + paymentDeadline: paymentDeadline, metadata: PrivateJsonObject(text: metadata) ), acceptedEventId: acceptedEventId, @@ -3672,6 +3743,7 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { rejectedOutboundStatus: nil, canceledEventId: nil, canceledOutboundStatus: nil, + conversionQuotes: [], paymentProofs: paymentProofs, lastStreamItemId: 1, lastOutboundMessageId: nil, @@ -3694,6 +3766,8 @@ final class PaykitPaymentRequestServiceTests: XCTestCase { paymentReference: PaymentReference(text: "invoice-123"), billingPeriod: billingPeriod, paymentEndpointIdentifier: endpoint, + allowanceId: nil, + conversionQuoteId: nil, proof: PrivateJsonObject(text: "{\"data\":\"proof\",\"type\":\"\(kind.rawValue)\"}"), recordedAt: "2027-01-15T08:01:00Z" ) diff --git a/BitkitTests/PaykitSubscriptionProposalTests.swift b/BitkitTests/PaykitSubscriptionProposalTests.swift index 8b2f549b1..20490f698 100644 --- a/BitkitTests/PaykitSubscriptionProposalTests.swift +++ b/BitkitTests/PaykitSubscriptionProposalTests.swift @@ -58,6 +58,8 @@ final class PaykitSubscriptionProposalTests: XCTestCase { endsAt: nil ), acceptedPaymentEndpointIdentifiers: ["bitcoin:regtest", "lightning:bolt11", "lightning:lnurl"], + conversion: nil, + paymentDeadline: nil, metadata: PrivateJsonObject(text: String(decoding: data, as: UTF8.self)) ) } diff --git a/BitkitTests/PaymentNavigationHelperTests.swift b/BitkitTests/PaymentNavigationHelperTests.swift index 0a2ac2110..340a411c4 100644 --- a/BitkitTests/PaymentNavigationHelperTests.swift +++ b/BitkitTests/PaymentNavigationHelperTests.swift @@ -256,6 +256,8 @@ final class PaymentNavigationHelperTests: XCTestCase { proposalExpiresAt: nil, recurrence: nil, acceptedPaymentEndpointIdentifiers: [endpointIdentifier], + conversion: nil, + paymentDeadline: nil, metadata: PrivateJsonObject(text: "{}") ), acceptedEventId: nil, @@ -264,6 +266,7 @@ final class PaymentNavigationHelperTests: XCTestCase { rejectedOutboundStatus: nil, canceledEventId: nil, canceledOutboundStatus: nil, + conversionQuotes: [], paymentProofs: [], lastStreamItemId: 1, lastOutboundMessageId: nil, diff --git a/Docs/paykit-issuer-interoperability.md b/Docs/paykit-issuer-interoperability.md index 526e49959..72f0d8869 100644 --- a/Docs/paykit-issuer-interoperability.md +++ b/Docs/paykit-issuer-interoperability.md @@ -13,10 +13,13 @@ An actionable request must satisfy all of these requirements: - The amount is a positive decimal Bitcoin value with at most eight significant fractional digits and no more than `18,446,744,073,709,551` satoshis. - The request is a one-time proposal: the local role is payer, lifecycle state is proposed, and recurrence is absent. - The proposal expiration is absent or is a valid future ISO 8601 timestamp. +- `paymentDeadline` is absent. Bitkit does not yet enforce actual-payment deadlines. - `acceptedPaymentEndpointIdentifiers` retains at least one identifier supported on the wallet's current network. Bitkit filters `acceptedPaymentEndpointIdentifiers` in issuer order, removes duplicates after their first occurrence, and drops unknown or wrong-network identifiers. The request remains actionable when at least one identifier survives. +Requests with a payment deadline remain visible in history but are unavailable for payment. This restriction is separate from proposal expiration, which controls acceptance. + ### Endpoint identifiers Lightning identifiers are chain-independent and are accepted on every network: diff --git a/changelog.d/next/823.fixed.md b/changelog.d/next/823.fixed.md new file mode 100644 index 000000000..748763dd5 --- /dev/null +++ b/changelog.d/next/823.fixed.md @@ -0,0 +1 @@ +Keep Paykit requests with unsupported payment deadlines in history and explain why their subscriptions cannot be accepted. diff --git a/journeys/payment-requests/README.md b/journeys/payment-requests/README.md index 7a9208498..6c0583bb6 100644 --- a/journeys/payment-requests/README.md +++ b/journeys/payment-requests/README.md @@ -48,7 +48,7 @@ That run established the issuer shapes captured by the fixture: lowercase `btc`, The failure reason vocabulary is: - Parse: `missing_local_role`, `outgoing_request`, `unsupported_local_role`, `missing_terms`, - `recurring_request`, `unsupported_asset`, `invalid_amount`, `amount_out_of_range`, + `recurring_request`, `unsupported_asset`, `unsupported_payment_deadline`, `invalid_amount`, `amount_out_of_range`, `no_supported_endpoint`, `invalid_expiration`, `expired`. - Resolution: `no_supported_endpoint`, `endpoint_not_payable`, `payment_details_pending`, `resolution_failed`. @@ -92,7 +92,7 @@ the SDK and can push the Pay step well past the budget. - Pending-request bell: `PaymentRequestsBell`. - Incoming sheet: `PaymentRequestsSheet`. - Screen: `PaymentRequestsScreen`. -- Request row: `PaymentRequestRow----one-time`; construct the complete value from the fixture issuer public key and negotiated receiver path because `wait-for-ui` does not support prefix matching. +- Request row: `PaymentRequestRow--one-time` for a one-time request; `wait-for-ui` requires the complete identifier. - Pay action: `PaymentRequestPay-`. - Dismiss action: `PaymentRequestDismiss-`. - Payment confirmation: `PaymentRequestConfirm`. @@ -105,3 +105,16 @@ the SDK and can push the Pay step well past the budget. - Payment Request amount screen: `PaymentRequestAmount`. - Terminal feedback: `PaymentRequestUnavailableToast`. - Expiration feedback: `PaymentRequestExpiredToast`. + +## Payment deadline history + +`payment-deadline-history.xml` covers rc56 requests with actual-payment deadlines. +Bitkit keeps their lifecycle and paid-period history, and subscription cancellation, +but does not accept them, offer payments, or schedule payment reminders. The journey +requires a controlled rc56 peer to prepare the accepted and paid records; repository +tests cover these states without sending funds. On both platforms, payment-history rows +show notes or dates rather than lifecycle labels, and active subscriptions are opened +from Overview. The journeys therefore record each fixture's payment request id, check +its full row identifier, and include the required back and tab transitions. The accepted +subscription must have no end date so cancellation is available. The proposal review +must explain that its payment details are unsupported and offer no Subscribe control. diff --git a/journeys/payment-requests/payment-deadline-history.xml b/journeys/payment-requests/payment-deadline-history.xml new file mode 100644 index 000000000..77a8de5aa --- /dev/null +++ b/journeys/payment-requests/payment-deadline-history.xml @@ -0,0 +1,37 @@ + + + Requests with actual-payment deadlines are visible but cannot be paid by this version of Bitkit. + Use a disposable Paykit test identity linked to a controlled rc56 peer. Prepare one-time BTC + requests with deadlines in proposed, accepted, rejected, canceled and proof-submitted states, + plus an incoming, accepted monthly BTC subscription with no end date, a period-start deadline, + one paid period and one unpaid period. Acceptance and proof submission must be prepared through the controlled + client because Bitkit intentionally cannot accept these terms. Use valid regtest payment proofs. + Do not reset or replace a funded wallet to prepare this fixture. Record the payment request id + for each one-time state because history rows show notes or dates instead of status labels. + + + Open Subscriptions and select the Payments tab after the controlled peer's records have synchronized + Verify the proposed row (id "PaymentRequestRow-<proposed-payment-request-id>-one-time") is visible + Verify the accepted row (id "PaymentRequestRow-<accepted-payment-request-id>-one-time") is visible + Verify the rejected row (id "PaymentRequestRow-<rejected-payment-request-id>-one-time") is visible + Verify the canceled row (id "PaymentRequestRow-<canceled-payment-request-id>-one-time") is visible + Verify the completed row (id "PaymentRequestRow-<completed-payment-request-id>-one-time") is visible + Open proposed row (id "PaymentRequestRow-<proposed-payment-request-id>-one-time") and verify no Pay action appears + Return to Payments + Open accepted row (id "PaymentRequestRow-<accepted-payment-request-id>-one-time") and verify no Pay action appears + Return to Payments + Select the Overview tab + Open the active subscription and verify its paid period remains visible and Cancel is available + Return to Subscriptions + Select the Payments tab + Verify the unpaid period is not offered for payment in the Payments tab or pending-request sheet + Have the controlled peer send a new subscription proposal with a period-start deadline + Select the Overview tab after the proposal has synchronized + Open that proposal and verify it explains that its payment details are not supported yet + Verify no Subscribe swipe control appears + Dismiss the proposal to return to Overview + Open the accepted subscription + Cancel the accepted subscription and verify its paid-period history remains accessible + Restart Bitkit and verify the one-time and subscription payment history is still visible + +