[Feat]: Android in-app PalsHub checkout + Play External Content Links - #766
Merged
Conversation
a-ghorbani
marked this pull request as ready for review
June 23, 2026 13:29
Mirror the iOS AuthSessionModule on Android: launch the Stripe checkout URL in a Chrome Custom Tab and resolve the existing NativeAuthSession spec with the captured pocketpal://checkout/* callback. A host=checkout BROWSABLE intent routes the callback to the module via MainActivity, not the hub deep-link path; a tab dismiss resolves to a silent cancel. Registered in MainApplication.getPackages() so the JS spec resolves; the Android checkout caller still uses the legacy web path until the buy flow is un-branched.
Wrap the Play Billing External Offers reporting handshake (createExternalOfferReportingDetailsAsync) behind an optional ExternalOfferModule, exposed to JS as NativeExternalOffer (TurboModuleRegistry.get, null on iOS). The call is best-effort: when the program is inactive or uncredentialed it is a logged no-op that still resolves, so reporting never blocks, fails, or queues a checkout. Not yet invoked from JS; the store hook follows.
After the reconcile poll confirms ownership, fire a best-effort External Offers report keyed on the purchase id. The report is fire-and-forget: a rejection is swallowed and never changes checkout state. It is skipped on iOS (spec null), on the already-owned (400) path (no external transaction), and on cancel/processing_deferred.
Un-branch handleBuyPress: the authenticated checkout now runs on both US iOS and US Android. The legacy Android Linking.openURL web path is removed. The auth guard runs first on both platforms; Android then shows a pre-purchase external-offers disclosure sheet before the Custom Tab opens, declining it starts nothing. iOS reaches checkout directly with no gate, unchanged.
Generalise the purchase-flow spec to both platforms and add a disclosure-consent helper. On Android the buy press routes through the pre-purchase disclosure gate into the Custom Tab; reaching the gate proves the native auth-session module is registered. Adds an Android-only declined-disclosure case (no checkout starts).
…olate state - target android.widget.EditText for email/password (Paper outlined inputs expose a non-editable container at the resource-id) - dismiss the native Android sign-in alert via android:id/button1 - relaunch the app per test for clean navigation state
The non-iOS Linking.openURL path that used it is gone; Android now runs the in-app checkout. Drops the function, its only env import, and its test.
start() had no epoch guard across the createCheckoutSession await: closing the sheet mid-create could reopen a stray browser tab and wedge the flow. Pin the epoch and bail if it moved. Cover the create and openAuth reject races.
Keep the buy-button spinner through the linking state (Play disclosure / link-out prep), and cover prep rejection + tokenless launch in tests.
a-ghorbani
force-pushed
the
feature/TASK-20260607-2338
branch
from
June 23, 2026 13:33
f6b00c8 to
658faab
Compare
There was a problem hiding this comment.
Pull request overview
Adds a unified, authenticated in-app PalsHub checkout flow for US Android by integrating Google Play’s External Content Links program, aligning Android behavior with the existing iOS CheckoutFlowStore flow and removing the legacy Android web link-out path.
Changes:
- Route Android “Buy” through
CheckoutFlowStoreand a new native Chrome Custom Tab auth-session module, includingpocketpal://checkout/*callback forwarding. - Add Android External Content Links preparation/reporting via a new TurboModule and bump Play Billing to
8.2.1. - Update unit/E2E tests to cover the new
linkingphase, cancellation/back-out behavior, and Android-specific UI/input handling.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/palshub-display.ts | Removes legacy “buy URL” helper used by the old Android browser link-out path. |
| src/utils/tests/palshub-display.test.ts | Removes tests for the deleted buy-URL helper. |
| src/store/CheckoutFlowStore.ts | Adds linking state, Android link-out prep + best-effort reporting, and epoch-guards to avoid stale callbacks. |
| src/store/tests/CheckoutFlowStore.test.ts | Adds coverage for linking outcomes, epoch-drop behavior, and Android/iOS-shaped paths. |
| src/specs/NativeExternalContentLink.ts | Introduces optional Android TurboModule spec for External Content Links prep/reporting. |
| src/components/PalsHub/PalDetailSheet/PalDetailSheet.tsx | Makes Buy start checkout on both platforms; includes linking in in-flight/loader logic. |
| src/components/PalsHub/PalDetailSheet/tests/PalDetailSheet.test.tsx | Updates tests for direct checkout on Android + new sign-in behavior; removes old web-link expectations. |
| e2e/specs/features/purchase-flow.spec.ts | Extends purchase flow E2E coverage to Android and adds an Android Custom Tab back-out scenario. |
| e2e/pages/PalPurchasePage.ts | Improves Android input targeting and adds adb-based foreground activity checks for Custom Tab dismissal tests. |
| android/app/src/main/java/com/pocketpalai/MainApplication.kt | Registers new TurboReactPackages for AuthSession and ExternalContentLink modules. |
| android/app/src/main/java/com/pocketpalai/MainActivity.kt | Forwards pocketpal://checkout/* intents to AuthSessionModule instead of RN Linking. |
| android/app/src/main/java/com/pocketpalai/ExternalContentLinkPackage.kt | Adds TurboReactPackage wiring for ExternalContentLinkModule. |
| android/app/src/main/java/com/pocketpalai/ExternalContentLinkModule.kt | Implements BillingClient External Content Links eligibility/token/link-out + best-effort reporting. |
| android/app/src/main/java/com/pocketpalai/AuthSessionPackage.kt | Adds TurboReactPackage wiring for AuthSessionModule. |
| android/app/src/main/java/com/pocketpalai/AuthSessionModule.kt | Implements Custom Tab checkout open + callback intent consumption and cancel-on-resume behavior. |
| android/app/src/main/AndroidManifest.xml | Adds pocketpal://checkout BROWSABLE intent filter for the checkout callback route. |
| android/app/build.gradle | Adds Custom Tabs dependency and bumps Play Billing to 8.2.1. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+56
to
+60
| val settled = AtomicBoolean(false) | ||
| val client = | ||
| BillingClient.newBuilder(appContext) | ||
| .enableBillingProgram(BillingProgram.EXTERNAL_CONTENT_LINK) | ||
| .build() |
Comment on lines
+4
to
+13
| export interface Spec extends TurboModule { | ||
| // Eligibility gate -> fresh external-transaction token -> Play link-out. | ||
| // outcome: 'launched' (open the URL), 'user_canceled', 'ineligible', 'error'. | ||
| prepareExternalLink(checkoutUrl: string): Promise<{ | ||
| outcome: string; | ||
| token?: string; | ||
| }>; | ||
| // Best-effort post-ownership report; a logged no-op while US enforcement is off. | ||
| reportExternalContentLink(purchaseId: string, token: string): Promise<void>; | ||
| } |
Comment on lines
+633
to
+656
| Platform.OS = 'android'; | ||
| (palStore as any).isUSRegion = true; | ||
| (authService as any).isAuthenticated = false; | ||
| const onSignInPress = jest.fn(); | ||
|
|
||
| const {getByTestId} = render( | ||
| <PalDetailSheet | ||
| {...defaultProps} | ||
| pal={mockPremiumPalsHubPal} | ||
| onSignInPress={onSignInPress} | ||
| />, | ||
| ); | ||
|
|
||
| await waitFor(() => { | ||
| expect(getByTestId('buy-button')).toBeTruthy(); | ||
| }); | ||
|
|
||
| fireEvent.press(getByTestId('buy-button')); | ||
|
|
||
| expect(onSignInPress).toHaveBeenCalled(); | ||
| expect(checkoutFlowStore.start).not.toHaveBeenCalled(); | ||
|
|
||
| Platform.OS = original; | ||
| }); |
Comment on lines
730
to
734
| await waitFor(() => { | ||
| expect(getByTestId('buy-button')).toBeTruthy(); | ||
| }); | ||
|
|
||
| fireEvent.press(getByTestId('buy-button')); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the authenticated in-app PalsHub purchase flow to US Android via Google
Play's External Content Links program (Billing 8.2.1+), matching the iOS
slice. The Android Buy button runs the same
CheckoutFlowStoremachinery as iOSinstead of opening the product page in the system browser.
This revises an earlier draft that targeted the wrong, deprecated program (EEA
External Offers, Billing 7.1.1, with an app-rendered disclosure sheet). It is now
the correct US program:
enableBillingProgram(EXTERNAL_CONTENT_LINK),launchExternalLink, and a Play-rendered disclosure — no app disclosure sheet.Linking.openURLweb path. US Android premium purchases reusecreateCheckoutSession+ the reconcile poll, opening the Stripe-hostedcheckout_urlin a Chrome Custom Tab via the thin KotlinAuthSessionModulethat satisfies the existing
NativeAuthSessionspec. Thepocketpal://checkout/*callback returns as ahost=checkoutBROWSABLE intent,forwarded from
MainActivity.onNewIntentto the module.ExternalContentLinkModule(
com.android.billingclient:billing:8.2.1) runs eligibility(
isBillingProgramAvailableAsync) -> fresh external-transaction token(
createBillingProgramReportingDetailsAsync, never cached) ->launchExternalLink(LINK_TO_DIGITAL_CONTENT_OFFER, CALLER_WILL_LAUNCH_LINK)before the Custom Tab. Google Play renders its own disclosure during that
call, so the app shows none. The store opens the Custom Tab only when the prep
returns
launched;user_canceled/ineligibleare silent cancels anderrorsurfaces a retryable error. A new transientlinkingstatus coversthis step and is included in both in-flight guards.
report fired only after the reconcile poll confirms ownership. It never blocks
or fails the checkout and is a logged no-op today (US reporting enforcement is
off); the token + launch path is built now.
its localized strings (Play renders the disclosure now).
(the existing US-region gate is untouched).
Dependencies (Android)
androidx.browser:browser:1.8.0(Chrome Custom Tabs) — unchangedcom.android.billingclient:billingbumped7.1.1->8.2.1(External ContentLinks APIs; 8.2.1 fixes the eligibility/token calls used here)
Verification
CheckoutFlowStore+PalDetailSheetsuites pass (93 tests). Newcoverage: the
linkingtransitions (launched-> browser,user_canceled/ineligible-> cancelled,error-> error), reset-during-linking drops theprep result, the in-flight guard includes
linking, the report fires once onreconcile-success ownership only (with the prep token) — not on the
already-owned path, cancel, or deferred — and an iOS-shaped path (no prep, no
report).
:app:assembleProdRelease(new arch) BUILD SUCCESSFUL(APK ~221 MB); the TurboModule codegen spec generated; billing resolved to
8.2.1.
stage (no iOS files changed in this revision).
Deferred (operational, not gating)
These mirror the iOS slice's deferred live round-trip and depend on Play Console
state, not on this code:
reported transactions appear — until then the report is a logged no-op by
design; a live report needs no app-code change beyond the server-side
submission.
has no wallet and auto-completes the test checkout). Note the un-enrolled
program may report ineligible/error in the emulator; the test stage records the
observed behaviour.
Generated by PocketPal Dev Team