Skip to content

feat: add a subscription clock offset to dev settings - #1341

Merged
jvsena42 merged 11 commits into
masterfrom
feat/demo-clock-subscriptions
Oct 2, 2026
Merged

jvsena42 merged 11 commits into
masterfrom
feat/demo-clock-subscriptions

Conversation

@ovitrif

@ovitrif ovitrif commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Twin: synonymdev/bitkit-ios#798

Adds a subscription clock offset to Dev Settings so a subscription renewal can be tested on a debug build without waiting a billing period.

Description

  • Adds Dev Settings > PAYKIT > "Subscription clock offset (days)" (Off, 1, 7, 30, 31, 62, 365) so subscription scheduling can be moved forward on a debug build
  • Applies the offset to Paykit subscription scheduling only: proposal expiry, due periods, renewal dates, due notifications and the payer's payment history, while one-time requests, invoices and payments keep real time
  • Keeps what a proposal publishes (recurrence start and anchor) and the stored acceptance time on real time, so turning the offset Off never drops or delays a period
  • Keeps the subscription review sheet naming the same first period that accepting pays, by taking the acceptance time on real time
  • Reschedules the due notifications and refreshes the subscriptions when the offset changes, notifying the latest period the jump made due per subscription, and the notification worker checks the subscription clock
  • Keeps the offset Off by default and ignores it outside debug builds, so release behaviour is unchanged

Out of Scope

  • Device clock: unchanged; moving the emulator clock breaks the Pubky sessions, which is why the offset lives in the app
  • Release builds: the row and the offset are unavailable there

Design

N/A — no design available.

Preview

Dev Settings row Next period due after a 31-day offset Two paid periods
Recording
subscription-clock-offset-second-billing-period.mp4

QA Notes

Journeys

  • temporary second-billing-period.xml — with a 31-day offset the next period comes due as a Payment Request and the subscription detail lists two payments
second-billing-period.xml
<journey name="Second Billing Period">
  <description>
    The next period of a monthly subscription comes due without waiting a month. Dev Settings carries
    a subscription clock offset, in debug builds only, that moves subscription scheduling forward by a
    number of days: proposal expiry, due periods, renewal dates and due notifications. One-time requests, invoices and payments keep real time, and the offset is Off by
    default. Requires the active "Journey Sub" from review-and-subscribe.xml with its first period paid
    on acceptance, and both instances on a debug build.
  </description>
  <actions>
    <action>Launch the E2E Bitkit app on both instances with "Journey Sub" active and its first period paid, per review-and-subscribe.xml</action>
    <action>On the creator instance, open the drawer menu, tap Settings (testTag "DrawerSettings"), then Dev Settings (testTag "DevSettings"), and scroll to the PAYKIT section</action>
    <action>Verify the "Subscription clock offset (days)" row (testTag "SubscriptionClockOffset") reads "Off"</action>
    <action>Tap the row and pick 31 (testTag "SubscriptionClockOffset-31"); verify the row now reads "31"</action>
    <action>Repeat the two previous steps on the payer instance</action>
    <action>On the payer instance, return home and verify within about ten seconds that the next period arrives as a Payment Request for "Journey Sub" over 5,000 sats, opening the Swipe To Pay sheet or waiting in the bell (testTag "PaymentRequestsBell")</action>
    <action>Pay it and verify Bitcoin Sent</action>
    <action>On the creator instance, verify the payment arrives, as the Received Instant Bitcoin sheet or a new row in Activity</action>
    <action>On the payer instance, open the drawer menu, tap Subscriptions and tap the "Journey Sub" row</action>
    <action>Verify the PAYMENTS section lists two payments, today's and one dated a month later, and that RENEWS moved a further month ahead</action>
    <action>On both instances, open Dev Settings again and set the subscription clock offset back to Off (testTag "SubscriptionClockOffset-0"); verify the row reads "Off"</action>
  </actions>
</journey>

Manual Tests

N/A

Automated Checks

  • added SubscriptionClockOffsetTest.kt — clamping, presets and the offset applied to a subscription clock
  • updated PaykitPaymentRequestRepoSubscriptionTest.kt — a subscription clock offset makes the next billing period due, lists the paid next period in the payment history, keeps the published start and the acceptance time on real time and keeps the first period due when accepting with the offset on
  • updated PaykitSubscriptionNotificationSchedulerTest.kt — an offset change reschedules queued notifications, notifies only the latest period it made due per subscription and still notifies the final period after a jump past the subscription end
  • updated SubscriptionsScreenTest.kt — the review sheet names the period accepting pays under a clock offset
  • updated PaykitPaymentRequestRepoTest.kt — the repo takes the subscription clock

@ovitrif ovitrif added this to the 2.7.0 milestone Oct 1, 2026
@ovitrif ovitrif changed the title feat: add a demo clock for subscription renewals feat: add a subscription clock offset to dev settings Oct 1, 2026
@ovitrif
ovitrif marked this pull request as ready for review October 1, 2026 18:07
@ovitrif
ovitrif requested review from jvsena42 and pwltr October 1, 2026 18:07
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T18:12:26.907270Z 9d06618 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d06618e96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@greptile-apps

This comment has been minimized.

Comment thread app/src/main/java/to/bitkit/utils/SubscriptionClockOffset.kt
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 0204818 (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@pwltr pwltr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two issues with the debug subscription clock that should be addressed before merge:

  1. MEDIUM — Reschedule due notifications when the subscription clock changes (app/src/main/java/to/bitkit/repositories/PaykitSubscriptionNotificationScheduler.kt:39). The scheduler reads the shifted clock, but synchronize() retains existing WorkManager jobs by name, so changing the offset does not update their original delay. A 31-day jump also moves the newly due period out of upcomingPeriodsAfter(now), while PaykitSubscriptionNotificationWorker checks the real clock and retries until the real start date. The renewal request can become due without its due notification firing. Please reschedule affected work when the offset changes and use the subscription clock in the worker. This is also raised inline.

  2. MEDIUM — Avoid publishing a shifted recurrence start (app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt:631). proposalDate comes from the offset clock, and buildSubscriptionProposal() publishes it as both startsAt and anchor. If the creator later turns the offset Off, or the payer has no matching offset, acceptance happens before that stored start. PaykitSubscriptionRecurrence.periodsThrough() then returns no first payment period until the future start date. Please keep the simulated time local to subscription scheduling rather than storing a shifted start in the shared proposal. This is also raised inline.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One LOW inline, for debug builds only.

Checked and clean:

  • Release gating: isAvailable = BuildConfig.DEBUG, subscriptionDate is a no-op when unavailable, SubscriptionClockOffsetSync.start() returns early, and the row is hidden. assembleDevRelease also has DEBUG=false.
  • @SubscriptionClock is injected only into PaykitPaymentRequestRepo and PaykitSubscriptionNotificationScheduler. No fee, LN invoice expiry, backup timestamp or one-time request deadline uses it: createPaymentRequest, isPending/isExpired, the updateRequest expiry check and incoming parsing all stay on the real clock.
  • No double pay: recurring request ids derive from the anchor (billingPeriod.startsAt) on both sides, and paid filtering goes through paidPeriods and the local completed and in-flight sets. The offset only widens which periods are generated.
  • No paying an expired request: recurring requests have no expiresAt, and under a shifted clock proposals can only look expired earlier.
  • No System.currentTimeMillis(), and the dev-settings strings are hardcoded as allowed.
  • Merges cleanly with current master. It will need a follow-up against #1401, whose new tests construct PaykitPaymentRequestRepo without the new subscriptionClock parameter.

Not raised: the offset persists after dev mode is switched off, the same as the other dev-only toggles on that screen.

Device gate: n/a — dev-settings-only change with no journey files.

Comment thread app/src/main/java/to/bitkit/repositories/PaykitPaymentRequestRepo.kt Outdated
@ovitrif

ovitrif commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

Pushed 3441108 (merges clean with master; no conflicts).

  • b4847f8: proposals publish real time for the recurrence start and anchor, and acceptance stores the real time, so only date carries the offset. Accepting with the offset on keeps the first period due, and turning it Off no longer drops or delays a period.
  • 3441108: a changed offset now cancels and re-enqueues the queued due notifications against the new clock, includes unpaid periods the jump made due, and the worker checks the subscription clock. SubscriptionClockOffsetSync refreshes the subscriptions when the offset changes instead of waiting for the next refresh.

@pwltr both MEDIUM items are addressed (scheduler and worker; shifted startsAt and anchor). @jvsena42 the first-period LOW is addressed with the accept-with-offset-on test you described. The Dev Settings journey stays a temporary journey in the description, so that thread is answered without a change.

@jvsena42 on #1401: it constructs PaykitPaymentRequestRepo without the new subscriptionClock parameter, so whichever PR lands second adapts; nothing changes here for it.

Local verification: full testDevDebugUnitTest (3070 passed) and detekt pass; the new tests fail on the previous code. The device re-run is in a later comment. The Preview media shows UI this push does not change.

@ovitrif
ovitrif requested review from jvsena42 and pwltr October 1, 2026 19:35

@pwltr pwltr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two notification-scheduling cases introduced by the clock-offset change still need fixes. The earlier recurrence-anchor, acceptance-time, worker-clock, and refresh concerns appear addressed on this head.

@ovitrif

ovitrif commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 719e139 (plus efb11ee, a comment shortened for the line-length check).

  • Catch-up alerts after an offset change are limited to the latest unpaid period per subscription, applied before the global cap of 32, so a long jump no longer queues many immediate alerts or crowds out upcoming reminders.
  • A subscription whose end the jump passes still gets the alert for its final unpaid period; only active subscriptions get upcoming reminders.

@pwltr this answers both new MEDIUM threads. Both new tests in PaykitSubscriptionNotificationSchedulerTest fail on the previous code. Local verification: full testDevDebugUnitTest (3072 passed) and detekt pass. The device re-run of the accept-with-offset-on and second-billing-period flows is still pending.

@ovitrif
ovitrif requested a review from pwltr October 1, 2026 20:37
@ovitrif

ovitrif commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Device re-run on an emulator with the dev debug build of efb11ee (one comment-only commit before the 719e139 scheduler edge-case fix, which neither flow touches), regtest, payer and creator as two Android users:

  • Second billing period: pass. With the offset at 31 on both, a 5,000 sat Payment Request reached the payer's bell within about 10 seconds and was paid. Both subscription details list two payments (October 1 and November 1) and RENEWS moved to December 1. Both offsets were set back to Off.
  • Accept with the offset on: partly verified. An offset of 31 cannot be set before accepting, because a proposal expires within at most 30 days and looks expired to the payer. At offset 7 the payer accepted and left the first period unpaid; after switching the offset Off that Payment Request was still pending and paid fine, which is the case the fix targets. The month-later period being due at acceptance is not verified on a device; the unit tests cover it.
  • Observation: the prompt that opened by itself over Dev Settings ignored three swipes, while paying the same request from the bell worked. Not investigated; it looks unrelated to this diff.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta since 9d06618e9: one LOW inline (debug only), a side effect of the acceptance-time fix.

  • The first-period LOW is fixed in b4847f8. Acceptance is stored on real time (:857), accept() returns the earliest pending period, and the restored-acceptance fallback uses clock.now(). The new test covers reset to Off.
  • pwltr's four MEDIUMs: published startsAt/anchor stay on real time, offset changes cancel and requeue work, there is one catch-up per subscription ahead of the global cap, and the final period past endsAt is kept.
  • No double pay after reset to Off. A period paid under the offset stays PROOF_SUBMITTED when real time reaches it, and request ids derive from the real-time anchor.
  • Release gating holds. isAvailable = BuildConfig.DEBUG, not dev mode, so a persisted offset is ignored in release builds even with dev settings unlocked.

Device gate: n/a — dev-settings-only change with no journey files.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 0204818: the subscription review sheet and its transition timer now take the acceptance time on real time (paymentDueOnAcceptance(now, acceptedAt)), so under an offset the sheet names the same first period that accepting pays. This answers @jvsena42's LOW on the review sheet; SubscriptionsScreenTest covers the shifted case.

Local verification: full testDevDebugUnitTest (3073 passed) and detekt pass. The device run in the earlier comment predates this label-only change.

@ovitrif
ovitrif requested a review from jvsena42 October 2, 2026 09:34

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delta since 719e139d4 (0204818): no findings. The review-sheet LOW is fixed.

  • paymentDueOnAcceptance(now, acceptedAt) is now requestsThrough(shifted now, real acceptedAt). That is the same pair accept() uses: it stores clock.now() and refreshes on the subscription clock. The sheet therefore names the period the swipe pays, and the new test covers it.
  • nextSubscriptionTransition gets the same real acceptedAt, so the label still refreshes at the right period end.
  • With the offset Off, the default acceptedAt = now leaves behaviour unchanged.

Device gate: n/a — dev-settings-only change with no journey files.

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved!

OBS: Could havesome AI instructions to improve discoverability by AI

@jvsena42
jvsena42 enabled auto-merge October 2, 2026 10:52
@jvsena42
jvsena42 merged commit 907f546 into master Oct 2, 2026
21 checks passed
@jvsena42
jvsena42 deleted the feat/demo-clock-subscriptions branch October 2, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants