# Payment logic rollback — 2026-10-15

## Result and scope

Both platforms return to the July purchase contract while retaining the current Pro screen's design, copy, comparison table, emojis, prices, legal links, Restore button and subscription-management button. The app keeps the current endpoint and version settings; local user edits to `utils/Config.tsx` and the Xcode version were preserved.

Historical references:

- Backend `7e20c43` — July 20, 2026, `google-play-fix`.
- App `54697c4` — pre-audit merge containing the July purchase logic and the existing restore flow. Installed `react-native-iap` remains 13.0.2.

This is a targeted rollback of checkout, restore and the Pro-status check, **not** a checkout of the entire old repositories. Analytics, onboarding, learning/review work and deployment tooling remain. The current app branch is `analytic-report`; the backend checkout is on `main`. Changes are local until committed/pushed and deployed through the usual process.

## Active behavior

1. Open Pro: connect to IAP and fetch the existing store products. Reading store history/validating transactions no longer happens automatically at app startup or paywall entry.
2. Subscribe: iOS directly calls `requestPurchase({sku})`; Android directly calls `requestSubscription` with the original first offer. No app-version policy, existing-subscription upgrade preflight, replacement-token check or historical-transaction recovery check runs first.
3. Send Apple's receipt to `/api/validate-receipt`, or Google's token to `/api/validate-receipt-android`, using the existing authenticated request fields.
4. On verified success, save `pro_mode` locally, finish/acknowledge as appropriate and navigate to the existing success screen.
5. Restore: explicitly query available purchases, prefer prov2 as in the earlier screen, revalidate on the backend and update the current plan. Restore never requests a new purchase.

Connections are paywall-scoped again. The two audit-era app-lifetime purchase providers and their navigation callbacks are unmounted. Their source and tests remain for history, but this rebuilt app does not execute them. Pro discovery reads the stored tier instead of depending on an IAP provider.

## Backend routing

| Endpoint | Active handler |
| --- | --- |
| POST `/api/validate-receipt` | `LegacyIOSPurchaseController::validateReceipt` |
| POST `/api/validate-receipt-android` | `LegacyAndroidPurchaseController::validateReceiptAndroid` |
| POST `/api/check-pro-status` | `LegacyIOSPurchaseController::checkProStatus` |
| POST `/api/android-purchase-policy` | Legacy Android handler; always returns compatibility for previously released audit clients |
| Apple/Google notification endpoints | Existing verified webhook handlers retained |

The active legacy repository uses classic `receipt`, `product_id`, `is_pro` and `users.pro_mode`. It does not require `receipt_hash`, store expiry/acknowledgement columns, or the webhook-events table for checkout. Existing added columns and rows are retained. The classic status check recognizes both tiers and ignores audit-era expiry metadata.

The historical payment controllers remain in the repository but are no longer bound to checkout/status routes. Their direct unit tests still run; the new route-binding tests verify the controllers selected by actual checkout URLs. The historical iOS recovery and Android upgrade endpoints remain for existing released clients, but the rebuilt July-style app does not call them. This patch does **not** remotely remove recovery logic from binaries already installed on customer devices; those users need the rebuilt app for the full client rollback.

## Deliberate safeguards retained

This is not byte-for-byte restoration of old bugs. Apple still verifies the receipt over HTTPS with the shared secret; the requested supported product's latest item is selected rather than blindly granting `in_app[0]`. Wrong-app, expired, refunded and upgraded-away items cannot grant access. The production `dev=true` bypass is not restored. Classic receipt ownership is preserved and repeated restores reuse the matching row. Pro2 counts as Pro. Notification delivery failure cannot turn an accepted purchase into an apparent failed payment. Analytics do not log raw tokens/receipts.

Android checkout comes from the July Google `subscriptionsv2` implementation with its supported-state/paid-expiry check and acknowledgement. Store-owned subscription replacement/upgrade automation from the audit is retired; the current comparison-page copy is retained as requested. In particular, test prov1-to-prov2 with the actual store and make no new guarantee of proration, automatic cancellation of the other SKU, or prevention of two concurrent subscriptions.

Read-only restore queries and backend HTTP validation have bounded waits to avoid indefinite loading; the payment request itself is never retried on a timer.

## Webhook boundary

July's webhook handlers had weaknesses in signature validation, event ordering and subscription-ID assumptions. They were not reintroduced. The existing verified webhook handlers and acknowledgement retry worker remain, including retries for purchases recorded before this rollback. No Apple/Google Console URL or live webhook configuration was changed.

Your production Apple webhook may continue targeting the previous production backend, provided it shares the intended production database and remains operational. The new v2 Apple webhook still requires its signing/trust/API configuration **if you send notifications there**. Checkout itself no longer depends on that configuration. Android renewal delivery fixes remain in place.

Do not assume two URLs isolate a shared database. Classic checkout no longer applies local expiry filtering; subsequent subscription changes rely on the active webhook's updates, as in the earlier flow. A missing or incorrect webhook update can leave stale access. Existing incorrect `users.pro_mode` data is not silently rewritten across all accounts; use a verified Restore for the original account and inspect a specific affected subscriber if needed.

## Deploy and test

1. Include all new legacy controller/repository files in the backend release. Deploy the changed routes with **migrate=false**. No database migration, data deletion or schema rollback is part of this patch.
2. Preserve the working `APPLE_SHARED_SECRET` and Google Play package/service-account credentials. iOS checkout no longer needs `.p8`, issuer ID, key ID, root-CA paths, numeric app ID or `IOS_PURCHASE_VALIDATION_MODE`. Existing Apple API settings can remain for the verified v2 webhook and historical recovery endpoint. No environment switch is needed to activate legacy checkout; routing selects it directly.
3. Refresh the application's route/config caches through the existing deployment process if required. Do not generate a new APP_KEY. Confirm the deployed release contains the new classes and route bindings; editing `.env` alone does not apply this code rollback.
4. Rebuild/install the mobile app. The endpoint currently remains whatever is selected in `utils/Config.tsx`; this patch preserved the user's `https://api.v2.appconjugate.com/api` value. Do not point a test at a different database unintentionally.
5. Test with dedicated Apple sandbox/Google license-testing accounts: fresh prov1, fresh prov2, user cancellation, declined validation, same-account restore, restart and correct Pro visibility. Do not buy again simply because a previous payment's server confirmation failed.
6. Test an existing paid subscriber on each platform and an older app against the deployed backend. Confirm existing purchase rows remain linked to the same Conju Gate account. Test renewal, paid-period cancellation and eventual expiry via the actual active webhooks.
7. Check prov1-to-prov2 separately: the audit's replacement flow is intentionally gone. Verify actual store behavior before claiming upgrades are supported as a single subscription.

Sandbox transactions sent to a production-database backend still create test rows in that database. Use dedicated accounts and do not clear production tables.

## Validation

New backend integration tests use memory-only SQLite with only the July schema, mocked Apple/Google HTTP, and no new Apple API credentials. They check both tiers, classic request fields, correct product selection, restore idempotence, account ownership, failed/expired/refunded receipt rejection, route bindings, compatibility policy and the blocked development bypass.

App tests exercise the real restored hook with mocked native stores/backend responses, checking the current layout and copy, original request arguments, success navigation, restore without payment, cancellation, and recovery from a restore error. Existing coordinator tests remain historical coverage; those coordinators are not mounted.

Device/store/server tests are still required. No production deployment, customer payment, database alteration, APK/IPA installation or Console change was performed while preparing this rollback.

The mobile suite completed 624 tests in 39 suites successfully. The remaining top-level `App.test.tsx` suite is blocked by Jest failing to transform Reanimated's ES module import. The same failure was reproduced using an untouched `HEAD:App.tsx` saved in an isolated temporary directory, confirming it predates this rollback. All changed app source files also passed Babel parsing. This does not substitute for a native device build/test.

Final checks: backend unit suite **366 tests / 1,952 assertions passed**. After adding a billing-connection failure regression check, the affected paywall/discovery suites were rerun: **38 tests passed**. The other mobile suites were unchanged after their successful full run.
