Skip to content

feat(mobile): add Apple Kilo Pass purchases#3079

Open
iscekic wants to merge 162 commits into
mainfrom
feat/apple-iap-personal-credit-purchases
Open

feat(mobile): add Apple Kilo Pass purchases#3079
iscekic wants to merge 162 commits into
mainfrom
feat/apple-iap-personal-credit-purchases

Conversation

@iscekic
Copy link
Copy Markdown
Contributor

@iscekic iscekic commented May 6, 2026

Summary

This PR replaces the original one-time Apple credit purchase experiment with App Store-managed Kilo Pass subscriptions across the data model, backend APIs, web management surfaces, and mobile purchase flow.

  • Adds provider-aware Kilo Pass persistence for Stripe vs store-managed subscriptions, including store purchase/event tables, provider subscription IDs, App Store account tokens, monthly App Store product metadata, and Google Play placeholders for the shared catalog.
  • Adds App Store server-side handling for signed transaction verification, idempotent purchase completion, renewals, cancellation state, failed renewals, upgrades/proration, refunds/revocations, credit clawbacks, and mobile-facing Kilo Pass store product/completion router methods.
  • Adds the iOS mobile Kilo Pass subscription experience: native modal presentation, App Store product loading, purchase/recovery hooks, account-token matching, ownership and cancellation states, App Store management routing, and profile dismissal/routing behavior after purchase.
  • Updates web Kilo Pass management so store-managed subscriptions route to App Store management instead of Stripe-only actions, and updates /device-auth to show the signed-in account plus a sign-out path before approving a device.
  • Adds Expo IAP/App Store server dependencies, env placeholders, generated DB migration artifacts, and focused tests around the new store subscription, notification, mobile purchase, and management flows.

Verification

  • App Store sandbox upgrade/downgrade sequence was manually checked against DB rows for subscription, store event, and credit ledger behavior in earlier branch validation.
  • Not manually re-verified during this PR-description update.

Visual Changes

Visual changes exist for the mobile Kilo Pass/profile purchase flow and web account-management surfaces, but screenshots are not attached in this description update.

Reviewer Notes

  • App Store behavior depends on production/sandbox config: root certificates, private key, key ID, issuer ID, Apple app ID, bundle ID, product IDs, subscription group, sandbox testers, and APPLE_IAP_ENVIRONMENT.
  • The current store catalog is monthly-only for App Store. Google Play product/base-plan metadata is present as a placeholder, but Google Play verification and notifications are not implemented here.
  • Store-managed subscriptions intentionally bypass Stripe portal, cancel, schedule, payment-method, and billing-history paths; please check those provider guards preserve existing Stripe behavior.
  • Purchase completion finishes StoreKit transactions only after backend completion so failed completions can be recovered on app start.
  • Refund/revocation handling reverses App Store base and bonus credits as negative ledger entries and depends on usable transaction price data from Apple.

iscekic added 30 commits May 6, 2026 14:36
@iscekic iscekic requested a review from pandemicsyn May 12, 2026 11:37
@iscekic
Copy link
Copy Markdown
Contributor Author

iscekic commented May 12, 2026

Review-ready status as of head 3e2fad9996d0dda9dba37f31a59548b76092236d:

  • All review threads are resolved.
  • CI is green: build, test, typecheck, lint, format-check, drizzle-check, check-unused, and trufflehog pass.
  • Extra local verification completed for the high-risk App Store paths: web targeted tests passed (196 tests), mobile targeted tests passed (268 tests), and scripts/typecheck-all.sh --changes-only escalated to full workspace typecheck and passed.
  • Manual review focus was App Store notification expiry/refund handling, purchase idempotency/account ownership, DB constraints, GDPR redaction, mobile purchase recovery, and provider-gated web management actions. No remaining actionable remarks found.

The only remaining merge blocker is required non-author approval.

@iscekic iscekic requested a review from marius-kilocode May 12, 2026 11:39
@iscekic
Copy link
Copy Markdown
Contributor Author

iscekic commented May 12, 2026

@pandemicsyn @marius-kilocode gentle ping: this is still blocked only on required non-author review. Current head 3e2fad9996d0dda9dba37f31a59548b76092236d has 42 checks passing, 0 failing, 0 pending; review threads are resolved.

Bot gone cray, sorry for the ping.

…al-credit-purchases

# Conflicts:
#	pnpm-workspace.yaml
Copy link
Copy Markdown
Contributor

@jeanduplessis jeanduplessis left a comment

Choose a reason for hiding this comment

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

Left one optional suggestion on state.ts about the read-named getter performing a DB write — feel free to take or leave it.

Comment thread apps/web/src/lib/kilo-pass/state.ts
iscekic added 14 commits May 12, 2026 19:41
Non-USD refunds (EUR, GBP, etc.) previously threw before writing
processed_at, causing Apple to retry forever and leaving subscriptions
active with no credit clawback.

- getAppleTransactionPriceMicrodollars returns null for non-USD instead
  of throwing; the stored amount_microdollars on the credit_transactions
  row for the original purchase is used as the base reversal amount
- When no stored amount is found for a non-USD refund, logs to Sentry
  and skips the base clawback rather than blocking the handler
- reverseAppStoreRefundCredits call is wrapped in try/catch so that any
  remaining error logs to Sentry and processing continues; subscription
  is always marked ended and processed_at is always written
…ForUser

App Store subscriptions have no stripeSubscriptionId, so the function was
returning no_subscription instead of a distinct reason. Added a new
store_managed_subscription reason variant (paymentProvider: 'apple') that
fires when paymentProvider !== 'stripe', updated both admin router consumers
and the bulk cancel UI to handle it.
…fications

Wrap the REFUND/REVOKE and renewal-completion dispatch branches each in a
single outer db.transaction so that credit reversals, subscription status
updates, audit log entries, and the processed_at write are committed or
rolled back as a unit.

- markStoreSubscriptionEnded now accepts a DbOrTx first arg and is exported
- reverseAppStoreRefundCredits drops its inner db.transaction; caller owns tx
- completeStoreKiloPassPurchase accepts an optional dbOrTx; wraps internally
  when called without one (backward-compatible for all existing callers)
- processAppStoreKiloPassNotification accepts an optional endStoreSubscription
  override (test-injection pattern matching decodeNotification etc.)
- Adds TDD test: rolls back credit reversal and processed_at write when
  markStoreSubscriptionEnded throws inside the outer transaction
Extract the lazy-cancel of store-managed subscriptions out of
`getKiloPassStateForUser` (which was performing a DB UPDATE inside a
read-named getter) and into a dedicated 15-minute cron at
`/api/cron/kilo-pass-store-subscription-reconcile`. The getter still
derives `canceled` status from the latest store-purchase `expires_at`
so callers see the correct state immediately, but no longer mutates
the row — read-only DB connections are now safe.
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