# Android purchase compatibility switch

17 September 2026. Implemented locally; **not deployed or release-approved**. Replaces the earlier forced-update preflight design.

## Controls

```dotenv
ANDROID_PURCHASE_NEW_FLOW_ENABLED=false
ANDROID_PURCHASE_MIN_APP_VERSION=1.4.6
```

`false` is the default/emergency fallback: purchases remain available through the separate compatibility receipt handler. `true` permits v2 only for valid installed versions >= 1.4.6 explicitly sending `purchase_flow=v2`. Old, missing, malformed or prerelease versions and requests without that mode use compatibility. `1.4.10` compares numerically above `1.4.6`. Raising the minimum selects compatibility, not a forced update. Invalid configuration disables v2, not all purchases.

Change the intended backend's environment file and refresh its Laravel configuration cache using the existing deployment procedure (`php artisan config:cache` in the active release, where supported). Restart any long-running app workers retaining old configuration. Verify the effective mode; editing `.env.example` or a local file does not change a deployed switch. No deployed environment file was changed during implementation.

## Contract

Authenticated `POST /api/android-purchase-policy` accepts native `app_version` and `app_build`. Example response:

```json
{"success":true,"allowed":true,"update_required":false,"min_app_version":"1.4.6","purchase_flow":"compatibility","reason":"compatibility"}
```

Eligible clients with the flag on receive `purchase_flow: "v2"`, `reason: "supported"`. Responses are not cacheable. Version is compatibility metadata, never authentication or proof of payment.

Receipt validation stays on `/api/validate-receipt-android`, requiring the existing authenticated receipt/product format. New clients also send native version/build and negotiated `purchase_flow`. Both handlers verify Google state and ownership before granting Pro. The flag is rechecked on every receipt request. Response header `X-Android-Purchase-Flow` reports the actual selected handler, which may differ from a previously negotiated request mode.

An executing request finishes with its selected handler; subsequent requests use the effective switch. An ambiguous failure does not automatically invoke a second handler. The switch does not create another payment.

## App behavior

The app saves mode in its existing durable purchase attempt **before** Google Play opens. Listener/restart/retry reuse that mode; the backend can override v2 to compatibility. Earlier saved attempts, manual restores and unbound recovery default to compatibility. Both handlers use the same stored entitlement format, so callbacks/jobs need no version-dependent protocol routing or new database columns.

An older allowed policy response with no mode means compatibility. Unknown modes, missing endpoints, network failure and malformed responses still block a NEW store request. Do not delete the policy endpoint when rolling back receipt logic. Already-paid validation/restore/recovery bypass policy. The parser supports older 426 responses during transition, but this updated backend no longer emits forced updates from its rollout minimum.

`utils/Config.tsx` remains the only manually selected API URL. No native/Gradle setting, version bump, UI, or analytics event changed for this switch. Rebuild the app: old APK/AAB outputs do not contain these edits.

## Scope: compatibility is not an exact historical rollback

`AndroidCompatibilityPurchaseValidator` is independent of the v2 receipt method and uses the active-purchase contract at commit `1561f12325358268d2798b90bae11fc7e597f2d5` as its reference. That is **not a confirmed live production revision**; actual previous app/production compatibility must still be accepted in QA.

It preserves receipt/product input and successful `success`, `message`, `expiryTime`, `acknowledged` fields; inactive compatibility responses retain HTTP 200/false. Deliberate safety differences include pending persistence/202, current expiry, shared ownership/idempotency/entitlement synchronization, durable acknowledgement retries, and safe errors. Cancellation preserves verified access until expiry; tracker failure does not fail a granted compatibility purchase.

The Google transport/OAuth service, repository, schema, authenticated webhook handler and acknowledgement worker remain shared. The flag does not disable their protections, roll back the installed coordinator, reverse data cleanup, isolate a shared-service bug, undo iOS changes, or change notification destinations. This is an **Android receipt-processing switch**, not a global old-backend switch. Existing schema prerequisites still apply in compatibility mode. No new migration or change to an existing migration is part of this implementation.

## Deployment and rollback

1. Confirm production's working revision/schema; preserve backups. Verify staging DB and notification isolation.
2. Resolve separate release decisions for existing shared/iOS changes and the duplicate-cleanup migration. The switch does not make them safe automatically.
3. Deploy the reviewed backend to staging with v2 OFF. Test previous Android/iOS apps and rebuilt Android 1.4.6.
4. Enable v2 in staging; test purchase, cancellation, pending, restore, restart and network recovery.
5. Disable v2 during an unfinished transaction; verify the same token grants access once, with acknowledgement recovery. Re-enable and repeat validation to check idempotency.
6. Deploy the reviewed compatible backend to production OFF, verify policy and old clients, then distribute the app gradually. Enable v2 deliberately after acceptance.
7. For a v2-specific regression, set false, refresh effective configuration and verify compatibility. Halt app rollout when needed. Do not roll migrations down, disable acknowledgement recovery, delete purchase rows, or ask paid users to pay again.

## Release blockers still open

- Confirm live baseline, staging/production database isolation and actual RTDN destination.
- Verify deployed policy availability; prior public probes returned 404.
- The separate [Android renewal webhook patch](android-renewal-webhook.md) fixes the RTDN `subscriptionId` requirement and Console `testNotification` rejection locally. Deploy it and verify authenticated Google delivery; the switch itself does not fix these or the separate iOS changes.
- Review the existing data-normalization migration and shared services.
- Run actual device/Play sandbox and production-like MySQL acceptance; local tests are not substitutes.

## Tests

```bash
php vendor/bin/phpunit --do-not-cache-result tests/Unit
```

The rollout integration test replaces the connection named `mysql` completely with in-memory SQLite and mocks Google/notifications. It covers pending v2 -> compatibility -> acknowledgement worker -> v2, duplicate receipts, ownership conflicts, expired subscriptions, legacy-row adoption, tracker failure and a supported webhook with the flag off. App tests cover persisted mode before payment, compatibility success responses and recovery after restart without another store request. No deployed database/store was contacted by these tests.
