Skip to content

feat: add subscription requests - #1239

Merged
jvsena42 merged 15 commits into
masterfrom
codex/paykit-subscription-requests-android
Sep 14, 2026
Merged

feat: add subscription requests#1239
jvsena42 merged 15 commits into
masterfrom
codex/paykit-subscription-requests-android

Conversation

@ben-kaufman

@ben-kaufman ben-kaufman commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds subscription proposals to contacts, building on the payer flow merged in #1186.

  • Create a daily, weekly, monthly or yearly subscription with an amount, name, optional description and optional public Pubky icon.
  • Reuse Payment Request amount entry, recipient rows and expiry controls; send to one saved, privately linked contact.
  • Distinguish queued from sent and pending from accepted; show creator subscriber/received-payment counts and support pending/active deletion without editing.
  • Validate the wire-size limit before icon upload/enqueue, downsample selected images, and aggregate valid billing-period proofs without creating payer prompts for creators.
  • Keep the newly merged screen map in sync and add one user-facing changelog fragment.

Discover, autopay and renewal UI are intentionally excluded. Icon hosting is public by design. Base is master; no unmerged payer dependency remains.

iOS counterpart: synonymdev/bitkit-ios#736.

Design

Pubky file safety

This branch uses paykit-rs v0.1.0-rc54. Every displayed Pubky URI, including a descriptor's Pubky src, uses the bounded Kotlin fetch with a 1 MiB successful-body limit before bytes cross the FFI boundary. Existing fixed display sizes, the 15% memory-cache limit and Coil's bounded dedicated disk cache remain in place. Custom subscription icons and public icon hosting remain unchanged.

The 1 MiB value caps each successful encoded body. It is not a total-memory or decoded-pixel limit. Paykit rejects oversized successful bodies while reading and detects declared-length truncation. HTTP error bodies may still be buffered inside the Pubky client before Paykit regains control. The request timeout limits duration rather than that error-body memory, transport and current-chunk buffers add overhead, and a close-delimited body can still return partial bytes if the connection closes early. Shared-avatar handling and ambiguous proposal-publication outcomes remain outside this dependency update.

Preview

Create → recipient → sent → overview using test wallets, silent at 4× speed. The first three seconds of the accelerated source clip were trimmed to exclude the system photo picker. All attached media was inspected and shows only Bitkit, including its keyboard; no device home screen or other app is included.

The walkthrough predates the final recipient corrections. The first screenshot below shows the final timer, Contacts spacing, Paste inset and 52dp field height.

android-pr-bitkit-only-4x.mp4
Final recipient, sent confirmation and overview screenshots Final recipient field, timer, Paste inset and Contacts spacing Subscription request sent confirmation Subscriptions overview after sending proposal

QA Notes

Manual Tests

  • 1. Subscriptions → Create Subscription → Amount: set a positive amount, name and frequency → Choose Recipient: only one saved/private-linked contact can be selected; expiry is configurable → Propose Subscription: confirmation shows recipient, amount and frequency.
  • 2. Create Subscription → upload custom icon → Choose Recipient → back: draft remains intact; the same public icon appears on the receiving device.
  • 3. Receiver → Review & Subscribe → accept and manually pay: creator shows one subscriber/one received payment attributed to the payer, without a payer payment prompt on the creator.
  • 4. Creator pending or accepted subscription → Delete → Swipe To Delete: request expires at the receiver and payment history remains available.
  • 5. Device offline → Propose Subscription: failure must not claim Sent; restore connectivity and retry the retained draft successfully.
  • 6. regression: Payment Requests → Create Payment Request: shared amount entry, contact selection and expiry remain functional.

Live regtest creation/delivery/acceptance/manual on-chain payments, custom icons and paid cancellation passed in both directions with iOS. Offline error/draft retention/retry and pending deletion also passed. Live transport-queued flushing, Lightning, mainnet and production push were not tested.

Automated Checks

  • Full suite: 2,485 unit tests passed with zero failures/skips on the current source, including ScreensMapTest.kt and seven PubkyImageFetcherTest.kt cases.
  • PubkyImageFetcherTest.kt verifies direct and descriptor-target fetches forward the 1 MiB limit and preserves coroutine cancellation. Native HTTP streaming behavior remains covered upstream in Paykit. PaykitSubscriptionProposalTest.kt covers UTF-8/wire-size boundaries. PaykitPaymentRequestRepoSubscriptionTest.kt covers creator terms, queued delivery, oversize rejection before icon upload/enqueue, pending cancellation and duplicate/off-schedule proofs. No automated coverage was removed.
  • Nine Compose tests passed: CreateSubscriptionScreenTest.kt, SubscriptionsScreenTest.kt and existing CreatePaymentRequestScreenTest.kt regression coverage. Includes all four frequency tabs, no-Discover empty state, compact 520dp confirmation layouts, loading state, single recipient, expiry, and truthful sent/queued copy.
  • Production and Android-test Kotlin compilation, rc54 native debug-symbol resolution and detekt completed with the dev flavor. Detekt retains its upstream advisory configuration. The cohesive shared subscription test fixture has a non-blocking LargeClass advisory, reviewed explicitly.
  • Final local review found no remaining actionable issues. The final UI changes reuse the existing Payment Request picker and shared subscription card; no changed-UI detekt findings remain.

@greptile-apps

greptile-apps Bot commented Sep 9, 2026

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

The PR should not merge until payment history for deleted creator subscriptions remains reachable.

Findings

  1. P1 Deletion hides payment history

Summary

  • Extends Paykit proposal terms and repository models to support recurring creator records.
  • Adds the multi-step Compose creation and confirmation flow.
  • Separates payer and creator subscription processing, notifications, and payment presentation.
  • Adds wire-size, repository, and Compose coverage.
  • Creator payment history becomes unreachable after deleting a paid subscription.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Create subscription details] --> B[Choose saved private-linked contact]
    B --> C[Validate proposal wire size]
    C --> D{Custom icon?}
    D -- Yes --> E[Compress and upload public icon]
    D -- No --> F[Build recurring Paykit proposal]
    E --> F
    F --> G[Enqueue proposal]
    G --> H{Delivered immediately?}
    H -- Yes --> I[Sent confirmation]
    H -- No --> J[Queued confirmation]
    G --> K[Creator subscription record]
    K --> L[Pending or active creator list]
    L --> M[Subscription details]
    M --> N[Received-payment history]
    M --> O[Delete or cancel]
    O --> P[Canceled record retained]
    P -. currently filtered from UI .-> N
Loading

@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.

Reviewed for fund draining specifically. No critical or high findings.

This adds the creator/payee side and re-tags the existing payer logic with isPayer guards. Every path that moves sats is untouched and still needs a fresh user action per period (acceptSubscriptionAndStartPayment → send flow; due periods → openIncomingPaymentRequest → send flow). Nothing auto-pays, nothing pays a cancelled or dismissed period, and the persisted-before-cleared dismissal ordering from the earlier fix is intact (dismissSubscriptionPayment, :289-322).

I specifically checked for the three bugs confirmed on the earlier subscriptions work — none repeat: creation uses a callback rather than a StateFlow, and the proof/dismissal code is unchanged, so neither the proof-kept-forever wedge nor the queue-cleared-before-persist ordering is reintroduced.

Also verified clean: amount shown vs paid is still enforced by acceptsPaymentAmount; cross-identity is guarded by expectedIdentity/generation under operationMutex with the SDK re-checking identity in uploadProfileAvatar and proposePaymentRequest, and publishCreatedSubscription gating on isCurrentState; payee records can't leak into one-off history (toPaykitPaymentRequest:1188 rejects recurrence != null); the notification scheduler, monthly-cost, proposals and accept paths are all isPayer-filtered; runSuspendCatching throughout the new suspend paths; creationMutex.tryLock guards double-submit.

Three LOW notes inline, all dev/QA-facing today (isPaykitEnabled default false). Two of them also apply to the iOS twin (synonymdev/bitkit-ios#736).

Comment thread app/src/main/java/to/bitkit/repositories/PaykitSubscription.kt
@jvsena42

Copy link
Copy Markdown
Member

conflicts

@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.

Re-reviewed at 70629e09. No new findings.

Attribution: the delta since my last pass (29cecb8a) is exactly one commit — 70629e095 chore: merge master into subscription requests — touching 34 files, of which 30 are byte-identical to master (#1216, #1235, #1236 landing) and 4 differ only on master's side. Net: this PR authored zero new lines since I last looked, so I re-verified the PR's own subscription code at head against the fund-safety list rather than reviewing a delta.

One merge interaction I checked because it's the kind of thing that slips through: master's #1216 made MethodId.fromRawValue(value, network = Env.network) network-aware, but PaykitSubscription.kt:313 still calls the one-arg form. The default parameter makes it behaviourally identical in production, and creator-side acceptedPaymentEndpointIdentifiers uses MethodId.rawValue = rawValueForNetwork(Env.network), so proposer and payer agree on identifiers. Not a finding, but worth a glance if the default ever changes.

Fund safety — the reason this PR gets the scrutiny it does. The creator-side role split only narrows the payer paths rather than adding new ones: requestsThrough returns empty for payee (:240), accept() requires it == subscription && it.isPayer && isProposalActionable and pins the displayed value (repo:690), the scheduler is gated on isPayer (scheduler:70), and restoredAcceptances is filtered on isPayer (repo:767). No auto-start charge path exists on the creator side. Period arithmetic holds: every == 0u rejected at :296, every.toLong() * index stays inside Long, and the plusX calls are wrapped in non-suspend runCatching — correct usage. No new ULong arithmetic was introduced.

Also clean: nothing seed-derived written, sent or logged; uploadProfileAvatar(expectedIdentity) and proposePaymentRequest(expectedIdentity) both check live identity, and publishCreatedSubscription gates on isCurrentState(generation, expectedIdentity); inbound proposal fields are all bounded (description 1024, benefits 8×160, note 256, icon_uri 512); every suspend path uses runSuspendCatching, with runCatching only on non-suspend helpers; dismissSubscriptionPayment persists before mutating in-memory state.

Recorded, not filed — the creator-side ledger counts payer-asserted proofs. PaykitSubscription.kt:345-354 reads only billingPeriod and paymentEndpointIdentifier; the SDK's proof payload is never inspected and nothing correlates against LDK or on-chain receipts, so a subscriber can make "Payments: N" and N +amount rows appear on the creator's detail screen without paying. Both platforms apply the same billing-grid filter before counting a proof (Android periodsThrough at PaykitSubscription.kt:352, iOS recurrence.contains(billingPeriod) at PaykitSubscription.swift:547-557), and Android derives the rows on the fly rather than persisting them. One Android-specific consequence worth knowing: isCreatedVisible (:216) includes || paidPeriods.isNotEmpty(), so a single bogus proof pins a cancelled subscription in the Created list.

This is the same pre-existing Paykit-wide trust model I recorded on the iOS twin (synonymdev/bitkit-ios#736) rather than filing against it — the SDK does stateless correlation only and delegates settlement to the caller. Not this PR's defect; this is just the first Android code to render the number. If it's ever addressed, the narrow interim is to keep paidPeriods.isNotEmpty() out of the delete-visibility rule, or label those rows as reported rather than settled.

Status of my open threads: the short-first-billing-period one (PaykitPaymentRequestRepo.kt:576) is still unanswered — head still has startsAt = timestamp, anchor = timestamp with timestamp = proposal time. The icon-namespace and orphaned-icon-blob threads you replied to today are both deferred with reasons I accept; I've left a note on the first with input from the iOS side for whenever the coordinated follow-up happens.

Gating unchanged: dev/QA-facing today.

@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.

Sent proposal sheet his not respecting top padding

Image

@jvsena42

Copy link
Copy Markdown
Member

Cross-platform test with iOS #736

Android 902bb335f (devDebug, Pixel 9 emulator) against iOS d2f0585f (iPhone 17 sim, Debug without E2E_BUILD → staging homeserver + staging regtest). Fresh identities: Android AA, iOS BB, saved as each other's contacts. Times are UTC.

Verdict: the Android side behaved correctly everywhere I could test it. The test IDs don't match iOS, which I'm treating as blocking, and the top-padding issue from my earlier review is still there.

Results against the QA list

# Test Result
1 Create & propose ✅ Received from iOS: Review & Subscribe opened on its own about 11 s after iOS sent (₿5,000, Weekly, first billing period correct). ✅ Android → iOS: the Sent screen shows BB, ₿1,000, Daily. deliveryStatus was Sent, but iOS never showed it (iOS blocker, details on #736).
2 Draft kept + icon ✅ Amount, Daily, name, description and the custom icon all survive Choose Recipient → Back. Couldn't check the icon on iOS because of the iOS blocker.
3 Accept & pay ✅ Paying an iOS-created subscription: Review & Subscribe → on-chain confirm (₿141 fee) → Subscribed. The overview shows Active 1 and a monthly cost of ₿21,667, which is correct for ₿5,000/week. The payment proof was queued and reached iOS about 25 s later, credited to AA.
4 Creator cancels ✅ iOS's delete reached Android in about 32 s: the status changed to Expired, Cancel disappeared, and the payment row (BB · −₿5,000) was kept under Expired.
5 Offline / oversized Not tested
6 Payment request regression ✅ Create Payment Request → BB → ₿1,000 → Send → Sent. It never appeared on iOS (iOS blocker).
7 Keyboard Not applicable on the emulator run

🔴 Blocking: IDs that don't match iOS

These have to match, per the shared journey vocabulary. AGENTS.md makes Android the reference, so most of the fixes may land on iOS (#736). Please settle each one with that PR so both land together.

Element Android iOS
Frequency tabs Tab-day/week/month/year (CustomTabRowWithSpacing.kt:58) Tab-daily/weekly/monthly/yearly (also changes with the device language on iOS)
Subscription amount step Reuses PaymentRequestAmountContent (CreateSubscriptionScreen.kt:151), so it exposes PaymentRequestAmount, PaymentRequestAmountContinue, PaymentRequestNumberPad SubscriptionAmount… prefix
Recipient contact row SubscriptionContact<pubky>, no separator (CreatePaymentRequestScreen.kt:490) SubscriptionContact-<pubky>

With the amount step reusing the PaymentRequest* tags, a journey can't tell the subscription amount step from the payment-request one on Android. Worth deciding on purpose.

Still open from my earlier review: top padding

It still reproduces at 902bb335f. The Sent proposal sheet's drag handle and "Sent" title are drawn inside the status bar. The Create Subscription sheet has the same problem: its title sits at status-bar height, right under the clock.

Other findings

  • The first review step doesn't name the sender. SubscriptionReviewSubscriptionProviderCard(subscription, contact) (SubscriptionsScreen.kt:702) shows only the contact's avatar ("B") next to the subscription name. The sender's name first appears on the on-chain confirm step. iOS fixed the equivalent in 123ccf51 (saved-contact name on review).
  • Link state for the iOS blocker, seen from Android. After iOS's link to AA got stuck ("Encrypted Link Handshake is still in progress"), Android kept calling restore_encrypted_link for BB and never started a new handshake. Removing and re-adding BB on Android didn't change that either; the SDK link state is independent of the contact list. There's no in-app recovery on either side. Full details and a repro are on Generic error message for Lightning payment failures hides actual cause #736.

Emulator note

The icon picker on the emulator is Google Photos' picker: tapping a tile only selects it, and it isn't applied until Done is tapped. The app correctly launches a single, image-only PickVisualMedia (GetContent is only the pre-Android-13 fallback), so this isn't a bug. It's just a trap when automating.

@ben-kaufman

Copy link
Copy Markdown
Contributor Author

Applied the standard sheet height to both the Create Subscription and Sent proposal states, so their titles keep the same top clearance as the other creation steps. Android's existing test tags remain unchanged as the shared journey reference. Sender identity and encrypted-link recovery remain separate from this layout fix.

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Regtest APK

Built from 2c96198 (run).

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

@jvsena42
jvsena42 dismissed their stale review September 13, 2026 23:34

Both blockers are resolved and verified, so this no longer applies. Top padding: fixed at 8f1e6f7 and confirmed on a Pixel 9 emulator running the PR-head CI build — the Create Subscription drag handle and title now clear the status bar and the Subscriptions top bar. Test IDs: iOS aligned to Android's tags in synonymdev/bitkit-ios#736 at 1833d00a (Tab-day/week/month/year from rawValue, so also locale-independent; PaymentRequest prefix on the amount step; contact-row hyphen dropped). Details in my new review.

@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.

Re-reviewed at 8f1e6f7eb. Both of my blockers are resolved — I've dismissed the stale CHANGES_REQUESTED. One new MEDIUM inline, gated so not blocking.

Top padding: fixed, and I verified it on device rather than from the diff. Installed the PR-head CI build (bitkit-dev-debug-188 from 8f1e6f7eb) on a Pixel 9 emulator: the Create Subscription drag handle and title now sit clear of both the status bar and the Subscriptions top bar. The fix is also complete — all four creation steps get the standard height now: Details (:150), Amount (:165, pre-existing), Recipient (:384, pre-existing), Sent (:204). SubscriptionProposalSent correctly takes modifier: Modifier = Modifier and applies it first in the chain (:433, :436), matching CreateSubscriptionDetails (:224, :241).

One honest gap: I could not reach the Sent proposal sheet on device this run. The recipient step showed SubscriptionRecipientUnavailable with Propose disabled — no privately-linked contact available, which is the stuck encrypted-link state I documented on synonymdev/bitkit-ios#736. So the Sent sheet is verified by code reading only: identical sheetHeight() application, and the layout cannot clip because it uses BoxWithConstraints(weight(1f)) -> verticalScroll + heightIn(min = maxHeight) (:452-456), with fillMaxSize() after a fixed height() being a no-op.

Test IDs: resolved on the iOS side, which is where the fixes belonged since AGENTS.md makes Android the reference. Verified at 1833d00a against the tags I read off the live emulator:

Element Android (on device) iOS at 1833d00a
Frequency tabs Tab-day / -week / -month / -year TabItem($0, accessibilityIdentifier: "Tab-\($0.rawValue)") with case day/week/month/year — matches, and now locale-independent since it keys off rawValue rather than the localized description
Amount step PaymentRequestAmount, PaymentRequestAmountContinue, PaymentRequestAmountField, PaymentRequestNumberPad testIdentifierPrefix: "PaymentRequest" — matches
Recipient contact row SubscriptionContact<pubky>, no separator hyphen dropped — matches

The new finding closes row 7 of my own QA table, which I had logged as "Keyboard — not applicable on the emulator run". It was never exercised; it is now, and there is a real gap. Inline at :244.

Checked and clean, at head:

  • Cancel and Back paths of the creation flow, enumerated. Details -> system Back hides the sheet and only remember state is lost; nothing remote or local exists yet. Amount -> Back and Recipient -> Back both return to Details with the draft intact (:160, :174). While a propose is in flight, scrim, drag and system Back are all blocked by ContentView.kt:502 dismissEnabled = !isCreatingPaymentRequest && … (honoured by SheetHost at :182 and :191), and creationMutex.tryLock (repo:528) blocks double-submit. Sent -> OK/Back just hides. An identity change or generation mismatch mid-proposal gives wasPublishedToActiveState=false -> sheet hidden plus a "queued" toast, with no local record created for a foreign identity (AppViewModel:5061-5071, repo:583, :677). An in-progress icon load gates both Choose Recipient and Propose (:330, :379).
  • Creator cancel/delete is remote-first: repo:1069 operation(current) then :1072 applySubscriptionRecordLocked, inside runSuspendCatching. A CancellationException landing between them leaves only a stale cache — the SDK record store, not local JSON, is the source of truth and is re-read by synchronizeLocked (:919). Delete goes through the SubscriptionCancel confirmation route (SubscriptionsScreen.kt:653-661). There is no "crash resurrects a cancelled subscription" path here.
  • Upgrade from v2.5.0. PaykitSubscription is derived, not persisted; the only persisted local state is subscriptionAcceptedAt / dismissedSubscriptionPaymentIds, and the repo diff touches no persistence code. The new model fields all default (role = Payer, iconUri = null, deliveryStatus = null), and role comes from the SDK's localRole with UNKNOWN/null mapping to null — the record is dropped rather than mis-assigned a role. No SDK bump: paykit-android 0.1.0-rc51, same as v2.5.0.
  • Fund safety, key material and trust boundaries are unchanged since my 70629e09 pass — this commit is layout-only and introduces no new state, suspend calls or arithmetic.

I also corrected a line in my 2026-09-10 review: I'd written that Android was stricter than iOS on the billing-grid check. Both platforms apply the same filter — Android periodsThrough at PaykitSubscription.kt:352, iOS recurrence.contains(billingPeriod) at PaykitSubscription.swift:547-557.

@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.

Re-reviewed at b62e0d402. The keyboard fix works — verified on device — but it covers only the Details step. The Recipient step has the same exposure and still reproduces. One MEDIUM inline, gated so not blocking.

Details step: fixed. Installed the PR-head CI build on a Pixel 9 and repeated the measurement from my earlier thread. With mInputShown=true on the Description field, SubscriptionChooseRecipient moved from [42,2172][1038,2319] (under the keyboard, top edge ~y1540) to [42,1352][1038,1499], and SubscriptionDescription from [42,1317][1038,1554] to [42,1115][1038,1352] — fully visible instead of clipped. The commit is exactly two lines, the import at :20 and .imePadding() at :246, placed inside the Column's chain as suggested.

Recipient step: same bug, still there. Same build, same device: tap SubscriptionRecipientSearch, keyboard opens, and SubscriptionPropose does not move — [42,2172][1038,2319] both before and after, entirely under the keyboard. Details inline on CreatePaymentRequestScreen.kt:415.

I checked the other two steps and they are genuinely not exposed, so this is the last one:

  • Amount (:153-167) uses the custom NumberPad (CreatePaymentRequestScreen.kt:185); NumberPadTextField.kt imports nothing from foundation.text or material3 and contains no TextField, so it never raises the IME.
  • Sent (:198-207) has no text input, and its chain at :437-444 is untouched.

Checked and clean:

  • The fix's ordering is correct and does not break the closed-keyboard case. modifier is the head of the chain at :242 and the call site at :151 passes Modifier.sheetHeight(), so the effective order is sheetHeight() -> fillMaxSize() -> … -> navigationBarsPadding() -> imePadding() -> padding() — fixed height outside the IME padding, which is the safe direction. imePadding() is inset-driven, so it pads by zero when the keyboard is hidden; nothing is pushed off the sheet. navigationBarsPadding() then imePadding() does not double-count, because the outer consumes the nav inset and the inner applies only the excess. SheetHost (BottomSheetScaffold, sheetPeekHeight = 0.dp, no windowInsets) supplies nothing, so there is no third application. This is byte-identical to the in-repo twin HwPassphrasePromptSheet.kt:121-127.
  • No scope creep. The diff since 8f1e6f7eb is two files; the .github/workflows/ci.yml hunk is master's #1241 arriving through the merge commit, not the author's change (git diff master refs/remotes/pr/1239 -- .github/workflows/ci.yml is empty). The propose path, creationMutex.tryLock (repo:528), the dismissEnabled = !isCreatingPaymentRequest guard (ContentView.kt:502) and draft preservation on Back (:161, :175) are all zero-line diffs.
  • Conventions on the touched code: modifier is the first optional parameter (:225, :435) and passed last with no trailing comma (:151, :166, :205, :386, :556).
  • CI is green on this head.

No test was added, and I am not asking for one — nothing in the repo's androidTest asserts IME layout, and a Compose-rule test cannot drive a real IME. This stays a device check.

@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.

Re-reviewed at c0caca6a9. Fixed, verified on device, and the shared-host regression risk checked rather than assumed. Nothing further from me on this PR.

The commit is the one-line .imePadding() at CreatePaymentRequestScreen.kt:417 plus its import — exactly the shape I suggested, in the shared content composable so it covers both hosts.

Measured on the PR-head CI build (Pixel 9, mInputShown=true): SubscriptionPropose moved from [42,2172][1038,2319] to [42,1352][1038,1499], i.e. from fully under the keyboard to above it. That was the last of the four creation steps — Details was fixed in the previous commit, and Amount and Sent have no IME exposure (the Amount step uses the custom NumberPad, and Sent has no text input).

The nested-imePadding() case is clean. Worth stating explicitly because I asserted it was safe when I proposed the fix, and an assertion is not evidence: PaymentRequestRecipientContent is also hosted inside ReceiveSheet, which already applies .imePadding() at ReceiveSheet.kt:130, so the new line now sits inside an existing one. Going Receive → Request from contact with the keyboard up, ReceiveScreen bottom goes 24241541 and PaymentRequestRecipient goes 23611541 — the inner container lands exactly on the keyboard edge. Double-counting would have shrunk it twice, to roughly y657. windowInsetsPadding consumes the inset for descendants, so the inner call contributes zero excess, and the bounds confirm it.

Also checked: the diff is that one file and two lines, so nothing else moved.

jvsena42
jvsena42 previously approved these changes Sep 14, 2026

@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.

✅ reviewed and cross tested with iOS PR

@jvsena42
jvsena42 enabled auto-merge September 14, 2026 14:21
jvsena42

This comment was marked as resolved.

@jvsena42

Copy link
Copy Markdown
Member

@ben-kaufman you have some commit with unverified signature

@ben-kaufman
ben-kaufman force-pushed the codex/paykit-subscription-requests-android branch from 92e836e to 2c96198 Compare September 14, 2026 20:44
@jvsena42
jvsena42 merged commit 364a8e0 into master Sep 14, 2026
19 checks passed
@jvsena42
jvsena42 deleted the codex/paykit-subscription-requests-android branch September 14, 2026 21:12
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.

2 participants