Skip to content

v6 Alpha.1 - #1225

Open
descorp wants to merge 21 commits into
v3from
feat/v6-alpha-example-testing
Open

descorp wants to merge 21 commits into
v3from
feat/v6-alpha-example-testing

Conversation

@descorp

@descorp descorp commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Complete public API redesign for the v6 alpha release of adyen-react-native. Rewrites the iOS/Android/TS bridge on top of Adyen's v6 core SDKs, replaces the React provider pattern with a static AdyenCheckout API, aligns callback signatures with the native iOS/Android SDKs, and adds the example app + CI infra needed to build and test it end to end.

Changes

Bridge rewrite (v6 core SDK)

  • Rebuild the bridge on v6's Checkout/session lifecycle: presenter-free callback bridging via a single CallbackBridge, consolidated native module wrapper hierarchy, checkout.invalidate() for abandoned flows
  • Rename AdyenContext→AdyenCheckout, SetupModule→ContextModule, EmbeddedComponentBusModule→ComponentModule, EmbeddedComponentDelegateProxy→ComponentProxy
  • Rename createSession→setup, setup→setupAdvanced, open→start
  • Replace per-type view components (CardView, GooglePayButton, ApplePayButton) with a generic <AdyenComponent>
  • Remove dead code: GooglePayModule, ApplePayModule, InstantModule, struct Payment (iOS)

Static API

  • Remove <AdyenCheckout> provider, useAdyenCheckout hook, and React context
  • Add static AdyenCheckout.setup() / setupAdvanced() returning a Checkout object; <AdyenComponent> reads from the checkout prop directly

Native alignment

  • Consolidate scattered static state into CheckoutState (both platforms); isSession auto-calculated from CheckoutContext type
  • Make cleanup() terminal-only, called only from onComplete/onError
  • Remove currentModule dispatch — DropIn routes directly from TS
  • Remove SessionErrorDelegate protocol (iOS)

Return-based callbacks

  • onSubmit returns Promise<SubmitResult> instead of receiving a handler param; onAdditionalDetails returns Promise<AdditionalDetailsResult>; onBeforeSubmit returns Promise<BeforeSubmitResult>
  • Factory helpers: SubmitResult.action() / .completed() / .retry()

DropIn simplification

  • Flatten the DropIn inheritance chain (remove PaymentComponentWrapper, AddressLookupModule)
  • Fix open() → start() name mismatch between TS and native modules
  • DropIn's delegate conformances are temporarily disabled — package-scoped symbols in the real Adyen module can't cross a precompiled xcframework boundary; DropIn is not fully supported in this alpha

Post-migration fixes

  • Fix a crash navigating away from an embedded <AdyenComponent>, a setup/setupAdvanced native method name mismatch, ComponentModule initializing after the first view could already mount, and missing BIN lookup/value events on iOS
  • iOS: vendor per-module xcframeworks instead of the umbrella Adyen pod, since the umbrella merges every module together and breaks canImport(AdyenCard)/canImport(AdyenComponents) checks for Card/Components payment methods. Requires use_frameworks! going forward, ~20 additional per-module imports, and working around BeforeSubmitData's inaccessible initializer by mutating the SDK-provided instance in place
  • Android: headless checkout.submit(type) (PayPal, Klarna) now presents any resulting action (e.g. a redirect) in a CheckoutFragment instead of having nowhere to render it, and reports a shopper cancellation if it's closed before completing
  • Gate Apple Pay's <AdyenComponent> mount behind a tap in the example app — its viewController is the PKPaymentAuthorizationViewController itself, so mounting it immediately presented the system sheet, unlike Google Pay's inline button

Example app & E2E

  • Add PayPal and Klarna to Sessions Components and Advanced Checkout; consolidate AvailablePaymentComponent/PayableComponent into a single PaymentMethodComponent
  • Add Appium E2E tests that fill in a test card and complete a real payment through Sessions Components and Advanced Checkout, on both Android and iOS
  • Remove the unused processError utility; rename processResult→navigateToResults

CI

  • Build and cache the vendored iOS xcframeworks: build_ios_frameworks.yml checks out adyen-ios at the pinned tag on a macOS runner, builds the per-module xcframeworks, and caches the result by version so most runs restore a tarball instead of rebuilding
  • Wire the cache into pr_check.yml for both build-lib (packaging ios/frameworks/ for e2e_test.yml) and test-iOS (pod install against example/ios); a cache miss fails loudly with a pointer to the new workflow's workflow_dispatch trigger rather than silently shipping a broken build
  • package.json's adyen.ios/adyen.android is now the single source of truth for pinned SDK versions, read via jq in workflows, Node in scripts/ensure-xcframeworks.sh, and Groovy's JsonSlurper in android/dependencies.gradle

Docs

  • Update docs/Architecture.md, add docs/native-architecture.md, docs/js-architecture.md, docs/public-api-flows.md, docs/v6-api-migration-summary.md, docs/v6-public-api-proposal.md, docs/consumer-migration-guide.md, docs/android-bridge-migration-guide.md, docs/ios-bridge-migration-guide.md

Testing

  • JS test suite passes; yarn lint and yarn typecheck clean
  • iOS unit tests updated (BaseModuleTests, ThreadingSafetyTests, ComponentModuleTests, and others) for the v6 module/delegate changes
  • New Appium E2E suite covers Sessions Components and Advanced Checkout card payments end to end on Android and iOS
  • Manual verification of PayPal, Klarna, Card, Apple Pay, and Google Pay flows in the example app

renovate Bot and others added 17 commits August 6, 2026 16:13
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.16 to 3.3.18.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/3.3.18/CHANGELOG.md)
- [Commits](ai/nanoid@3.3.16...3.3.18)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-version: 3.3.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
…3.18

chore(deps): bump nanoid from 3.3.16 to 3.3.18
fix(deps): update dependency react-native-screens to v4.26.2
chore(deps): update yarn to v4.17.1
chore(deps): update dependency prettier to ^3.9.5
Rebuilds the bridge on the v6 core SDK: new Checkout/session lifecycle, presenter-free callback bridging via a single CallbackBridge, consolidated native module wrapper hierarchy, checkout.invalidate() for abandoned flows, and the AdyenContext to AdyenCheckout module rename. Updates docs and the architecture contract to match.
Fixes a crash navigating away from an embedded AdyenComponent, a setup/setupAdvanced native method name mismatch, ComponentModule initializing after the first AdyenComponent view could already mount, and missing BIN lookup/value events on iOS.
Consolidates AvailablePaymentComponent/PayableComponent into a single PaymentMethodComponent, adds PayPal and Klarna to Sessions Components and Advanced Checkout, adds testIDs for E2E, fixes the stale @react-native/typescript-config path, and navigates with the SDK-provided session result directly.
Appium tests that fill in a test card and complete a real payment through Sessions Components and Advanced Checkout, on both Android and iOS.
…n pod

The umbrella Adyen pod merges every module into one, so canImport(AdyenCard)/canImport(AdyenComponents) are false and Card/Components payment methods fail to build. Vendors 9 separate xcframeworks built via scripts/build-xcframeworks.sh (run automatically by 'yarn app pod'; not committed). Requires use_frameworks! in the Podfile going forward, plus ~20 additional per-module imports throughout the bridge that the umbrella merge previously papered over. Also disables DropIn's delegate conformances (package-scoped symbols can't cross a precompiled xcframework boundary) and works around BeforeSubmitData's inaccessible initializer by mutating the SDK-provided instance in place.
Mounting <AdyenComponent type="applepay"> presented the system Apple Pay sheet immediately: its viewController is the PKPaymentAuthorizationViewController itself, not an inline button like Android's Google Pay. Renders a button that mounts the component only once tapped.
…eadless submit()

checkout.submit(type) for headless payment methods (PayPal, Klarna) had no UI hosting the controller, so a resulting action (e.g. a redirect) had nowhere to render. Shows the action in a CheckoutFragment and reports a shopper cancellation if it's closed before completing.
adyen-react-native.podspec vendors per-module xcframeworks instead of the umbrella Adyen pod, built from a local adyen-ios checkout. CI needs the same thing: build_ios_frameworks.yml checks out adyen-ios at the pinned tag on a macOS runner and builds them, caching the result by version so most runs just restore a tarball instead of rebuilding.

Wires it into pr_check.yml ahead of build-lib (needed there to package ios/frameworks/ into the tgz for e2e_test.yml, gated by build-tgz) and test-iOS (needed there for pod install against example/ios directly). Both restore the same cache key rather than rebuilding themselves; a cache miss fails loudly with a pointer to run the new workflow's workflow_dispatch trigger rather than silently shipping a broken build.

Adds package.json's adyen.ios/adyen.android as the single source of truth for these pinned versions, instead of duplicating them across the podspec comment, three workflow files, and android/dependencies.gradle. Read via jq in workflows, node in scripts/ensure-xcframeworks.sh, and Groovy's built-in JsonSlurper in dependencies.gradle.
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

🟡 Public API changes detected

🆕 Added (10)

  • AdditionalDetailsResult — const
  • AdvancedCallbacks — interface
  • AdvancedPayment — interface
  • AdyenComponentProps — interface
  • BeforeSubmitData — interface
  • BeforeSubmitResult — const
  • Checkout — interface
  • PaymentResult — interface
  • SessionCallbacks — interface
  • SubmitResult — const

❌ Removed (22)

  • AdyenActionComponent — interface
  • AdyenApplePay — const
  • AdyenCheckoutContextType — interface
  • AdyenCheckoutProps — type
  • AdyenGooglePay — const
  • AdyenInstant — const
  • ApplePayButton — const
  • ApplePayButtonProps — interface
  • ApplePayButtonTheme — const
  • ApplePayButtonType — const
  • ApplePayMerchantCapability — type
  • ApplePayModule — interface
  • CardView — const
  • CardViewProps — interface
  • GooglePayButton — const
  • GooglePayButtonProps — interface
  • GooglePayButtonTheme — const
  • GooglePayButtonType — const
  • GooglePayModule — interface
  • HideOption — interface
  • InstantModule — interface
  • useAdyenCheckout — const

✏️ Modified (7)

  • AdyenCheckout — class
  • AdyenComponent — const
  • ApplePayConfiguration — interface
  • DropInModule — interface
  • Event — enum
  • PartialPaymentComponent — interface
  • UNSUPPORTED_PAYMENT_METHODS — const

🆕 Methods added (3)

  • AdyenCheckout.setup
  • AdyenCheckout.setupAdvanced
  • DropInModule.start

❌ Methods removed (1)

  • AdyenComponent.hide

🆕 Parameters added (7)

  • AdyenCheckout.setup(callbacks)
  • AdyenCheckout.setup(configuration)
  • AdyenCheckout.setup(session)
  • AdyenCheckout.setupAdvanced(callbacks)
  • AdyenCheckout.setupAdvanced(configuration)
  • AdyenCheckout.setupAdvanced(paymentMethods)
  • DropInModule.start(checkout)

❌ Parameters removed (2)

  • AdyenComponent.hide(option)
  • AdyenComponent.hide(success)

🆕 Enum cases added (1)

  • Event.onBeforeSubmit
API snapshot diff
--- /tmp/base-api-report.md	2026-09-22 13:23:30.381476936 +0000
+++ etc/api/adyen-react-native.api.md	2026-09-22 13:22:56.065561583 +0000
@@ -4,11 +4,7 @@
 
 ```ts
 
-import { JSX } from 'react';
 import { default as React_2 } from 'react';
-import { ReactNode } from 'react';
-import { StyleProp } from 'react-native';
-import { ViewStyle } from 'react-native';
 
 // @public
 export interface ActionModule {
@@ -17,6 +13,17 @@
     threeDS2SdkVersion: string;
 }
 
+// @public
+export interface AdditionalDetailsResult {
+    // (undocumented)
+    readonly resultCode: string;
+}
+
+// @public
+export const AdditionalDetailsResult: {
+    readonly completed: (resultCode: string) => AdditionalDetailsResult;
+};
+
 // @public (undocumented)
 export const ADDRESS_COMPONENTS: string[];
 
@@ -39,42 +46,36 @@
 export type AddressMode = 'full' | 'postalCode' | 'none' | 'lookup';
 
 // @public
-export const AdyenAction: ActionModule;
+export interface AdvancedCallbacks {
+    onAdditionalDetails(data: PaymentDetailsData): Promise<AdditionalDetailsResult>;
+    onComplete(result: PaymentResult): void;
+    onError(error: AdyenError): void;
+    onSubmit(data: PaymentMethodData_2): Promise<SubmitResult>;
+}
 
 // @public
-export interface AdyenActionComponent extends AdyenComponent {
-    handle(action: PaymentAction): void;
+export interface AdvancedPayment {
+    action(action: PaymentAction): void;
+    completion(resultCode: string): void;
+    retry(message?: string): void;
 }
 
 // @public
-export const AdyenApplePay: ApplePayModule;
-
-// @public (undocumented)
-export const AdyenCheckout: React_2.FC<AdyenCheckoutProps>;
+export const AdyenAction: ActionModule;
 
 // @public
-export interface AdyenCheckoutContextType {
-    config: Configuration;
-    isReady: boolean;
-    paymentMethods?: PaymentMethodsResponse;
-    start: (typeName: string) => void;
+export class AdyenCheckout {
+    static setup(session: SessionConfiguration, configuration: Configuration, callbacks: SessionCallbacks): Promise<Checkout>;
+    static setupAdvanced(paymentMethods: PaymentMethodsResponse, configuration: Configuration, callbacks: AdvancedCallbacks): Promise<Checkout>;
 }
 
 // @public
-export type AdyenCheckoutProps = {
-    config: Configuration;
-    paymentMethods?: PaymentMethodsResponse;
-    session?: SessionConfiguration;
-    onSubmit?: (data: PaymentMethodData_2, component: AdyenActionComponent, extra?: any) => void;
-    onError: (error: AdyenError, component: AdyenComponent) => void;
-    onAdditionalDetails?: (data: PaymentDetailsData, component: AdyenActionComponent) => void;
-    onComplete?: (result: SessionsResult, component: AdyenComponent) => void;
-    children: ReactNode;
-};
+export const AdyenComponent: React_2.FC<AdyenComponentProps>;
 
 // @public
-export interface AdyenComponent {
-    hide(success: boolean, option?: HideOption): void;
+export interface AdyenComponentProps {
+    checkout: Checkout;
+    type: string;
 }
 
 // @public
@@ -101,12 +102,6 @@
 }
 
 // @public
-export const AdyenGooglePay: GooglePayModule;
-
-// @public
-export const AdyenInstant: InstantModule;
-
-// @public
 export interface AnalyticsOptions {
     enabled?: boolean;
     verboseLogs?: boolean;
@@ -123,58 +118,10 @@
 
 // @public
 export interface ApplePayAuthorizationResult {
-    // (undocumented)
     errors?: ApplePayError[];
-    // (undocumented)
     status: 'success' | 'failure';
 }
 
-// @public (undocumented)
-export const ApplePayButton: (input: ApplePayButtonProps) => JSX.Element;
-
-// @public (undocumented)
-export interface ApplePayButtonProps {
-    // (undocumented)
-    onPress?: () => void;
-    // (undocumented)
-    radius?: number;
-    // (undocumented)
-    style?: StyleProp<ViewStyle>;
-    // (undocumented)
-    theme?: keyof typeof ApplePayButtonTheme;
-    // (undocumented)
-    type?: keyof typeof ApplePayButtonType;
-}
-
-// @public (undocumented)
-export const ApplePayButtonTheme: {
-    readonly WHITE: 1;
-    readonly WHITE_OUTLINE: 2;
-    readonly AUTOMATIC: 3;
-    readonly BLACK: 4;
-};
-
-// @public (undocumented)
-export const ApplePayButtonType: {
-    readonly BUY: 1;
-    readonly SETUP: 2;
-    readonly INSTORE: 3;
-    readonly DONATE: 4;
-    readonly CHECKOUT: 5;
-    readonly BOOK: 6;
-    readonly SUBSCRIBE: 7;
-    readonly RELOAD: 8;
-    readonly ADDMONEY: 9;
-    readonly TOPUP: 10;
-    readonly ORDER: 11;
-    readonly RENT: 12;
-    readonly SUPPORT: 13;
-    readonly CONTRIBUTE: 14;
-    readonly TIP: 15;
-    readonly CONTINUE: 16;
-    readonly PLAIN: 0;
-};
-
 // @public
 export type ApplePayCalendarUnit = `year` | `month` | `day` | `hour` | `minute`;
 
@@ -183,7 +130,6 @@
     allowOnboarding?: boolean;
     billingContact?: ApplePayPaymentContact;
     couponCode?: string;
-    merchantCapabilities?: ApplePayMerchantCapability[];
     merchantID: string;
     merchantName?: string;
     onAuthorize?: (payment: ApplePayPaymentAuthorization, actions: ApplePayAuthorizationActions) => void;
@@ -222,21 +168,6 @@
 }
 
 // @public
-export type ApplePayMerchantCapability = 'debit' | 'credit';
-
-// @public (undocumented)
-export interface ApplePayModule extends AdyenComponent, ConditionalPaymentComponent {
-    // (undocumented)
-    provideAuthorizationResult(result: ApplePayAuthorizationResult): void;
-    // (undocumented)
-    provideCouponCodeUpdate(update: ApplePayCouponCodeUpdateRequest): void;
-    // (undocumented)
-    provideShippingContactUpdate(update: ApplePayShippingContactUpdateRequest): void;
-    // (undocumented)
-    provideShippingMethodUpdate(update: ApplePayShippingMethodUpdateRequest): void;
-}
-
-// @public
 export interface ApplePayPaymentAuthorization {
     billingContact?: ApplePayPaymentContact;
     shippingContact?: ApplePayPaymentContact;
@@ -328,6 +259,36 @@
     locale?: string;
 }
 
+// @public
+export interface BeforeSubmitData {
+    // (undocumented)
+    billingAddress?: object;
+    // (undocumented)
+    deliveryAddress?: object;
+    // (undocumented)
+    shopperEmail?: string;
+    // (undocumented)
+    shopperName?: {
+        firstName?: string;
+        lastName?: string;
+    };
+}
+
+// @public
+export type BeforeSubmitResult = {
+    readonly type: 'proceed';
+    readonly data: BeforeSubmitData;
+    readonly sessionData?: string;
+} | {
+    readonly type: 'abort';
+};
+
+// @public
+export const BeforeSubmitResult: {
+    readonly proceed: (data: BeforeSubmitData, sessionData?: string) => BeforeSubmitResult;
+    readonly abort: () => BeforeSubmitResult;
+};
+
 // @public (undocumented)
 export interface BinLookupData {
     // (undocumented)
@@ -365,11 +326,13 @@
 }
 
 // @public
-export const CardView: React_2.FC<CardViewProps>;
-
-// @public (undocumented)
-export interface CardViewProps {
-    paymentMethod?: PaymentMethod;
+export interface Checkout {
+    readonly configuration: Configuration;
+    invalidate(): void;
+    isAvailable(type: string): Promise<boolean>;
+    readonly paymentMethods: PaymentMethodsResponse;
+    requiresUserInteraction(type: string): Promise<boolean>;
+    submit(type: string): void;
 }
 
 // @public
@@ -401,9 +364,10 @@
 }
 
 // @public
-export interface DropInModule extends AdyenActionComponent {
+export interface DropInModule extends AdvancedPayment {
     getReturnURL: () => Promise<string>;
     providePaymentMethods(paymentMethods: PaymentMethodsResponse, order: Order | undefined): void;
+    start(checkout: Checkout): void;
 }
 
 // @public
@@ -437,6 +401,7 @@
     onApplePayCouponCodeChange = "didUpdateCouponCodeCallback",
     onApplePayShippingContactChange = "didUpdateShippingContactCallback",
     onApplePayShippingMethodChange = "didUpdateShippingMethodCallback",
+    onBeforeSubmit = "didBeforeSubmitCallback",
     onBinLookup = "didBinLookupCallback",
     onBinValue = "didChangeBinValueCallback",
     onCancelOrder = "didCancelOrderCallback",
@@ -464,41 +429,6 @@
 }
 
 // @public (undocumented)
-export const GooglePayButton: (input: GooglePayButtonProps) => JSX.Element;
-
-// @public (undocumented)
-export interface GooglePayButtonProps {
-    // (undocumented)
-    onPress?: () => void;
-    // (undocumented)
-    radius?: number;
-    // (undocumented)
-    style?: StyleProp<ViewStyle>;
-    // (undocumented)
-    theme?: keyof typeof GooglePayButtonTheme;
-    // (undocumented)
-    type?: keyof typeof GooglePayButtonType;
-}
-
-// @public (undocumented)
-export const GooglePayButtonTheme: {
-    readonly DARK: 1;
-    readonly LIGHT: 2;
-};
-
-// @public (undocumented)
-export const GooglePayButtonType: {
-    readonly BUY: 1;
-    readonly BOOK: 2;
-    readonly CHECKOUT: 3;
-    readonly DONATE: 4;
-    readonly ORDER: 5;
-    readonly PAY: 6;
-    readonly SUBSCRIBE: 7;
-    readonly PLAIN: 8;
-};
-
-// @public (undocumented)
 export interface GooglePayConfiguration {
     allowCreditCards?: boolean;
     allowedAuthMethods?: CardAuthMethod[];
@@ -523,21 +453,12 @@
     Test = 3
 }
 
-// @public (undocumented)
-export interface GooglePayModule extends ConditionalPaymentComponent, AdyenActionComponent {
-}
-
 // @public
 export interface GooglePayShippingAddressParameters {
     allowedCountryCodes?: string[];
     phoneNumberRequired?: boolean;
 }
 
-// @public
-export interface HideOption {
-    message?: string;
-}
-
 // @public (undocumented)
 export interface InstallmentOption {
     // (undocumented)
@@ -556,10 +477,6 @@
 export type InstallmentPlan = 'regular' | 'revolving';
 
 // @public (undocumented)
-export interface InstantModule extends AdyenActionComponent {
-}
-
-// @public (undocumented)
 export const NATIVE_COMPONENTS: string[];
 
 // @public (undocumented)
@@ -570,7 +487,7 @@
 }
 
 // @public (undocumented)
-export interface PartialPaymentComponent extends AdyenComponent {
+export interface PartialPaymentComponent extends AdvancedPayment {
     // (undocumented)
     provideBalance(success: boolean, balance: Balance | undefined, error: Error | undefined): void;
     // (undocumented)
@@ -685,6 +602,15 @@
     storedPaymentMethods?: StoredPaymentMethod[];
 }
 
+// @public
+export interface PaymentResult {
+    // (undocumented)
+    [key: string]: unknown;
+    action?: PaymentAction;
+    refusalReason?: string;
+    resultCode?: ResultCode;
+}
+
 // @public (undocumented)
 export interface PostalAddress {
     city?: string;
@@ -712,6 +638,13 @@
 }
 
 // @public
+export interface SessionCallbacks {
+    onBeforeSubmit?(data: BeforeSubmitData): Promise<BeforeSubmitResult>;
+    onComplete(result: SessionsResult): void;
+    onError(error: AdyenError): void;
+}
+
+// @public
 export interface SessionConfiguration {
     // (undocumented)
     id: string;
@@ -741,6 +674,25 @@
     paymentData: PaymentMethodData_2;
 }
 
+// @public
+export type SubmitResult = {
+    readonly type: 'action';
+    readonly action: PaymentAction;
+} | {
+    readonly type: 'completed';
+    readonly resultCode: string;
+} | {
+    readonly type: 'retry';
+    readonly message?: string;
+};
+
+// @public
+export const SubmitResult: {
+    readonly action: (action: PaymentAction) => SubmitResult;
+    readonly completed: (resultCode: string) => SubmitResult;
+    readonly retry: (message?: string) => SubmitResult;
+};
+
 // @public (undocumented)
 export interface ThreeDSConfiguration {
     requestorAppUrl?: string;
@@ -752,9 +704,6 @@
 // @public (undocumented)
 export const UNSUPPORTED_PAYMENT_METHODS: string[];
 
-// @public
-export const useAdyenCheckout: () => AdyenCheckoutContextType;
-
 // (No @packageDocumentation comment for this package)
 
</details>

Please review the changes and update `etc/api/adyen-react-native.api.md` if this public API change is intentional.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request migrates the React Native SDK to Adyen iOS and Android SDKs v6.0.0-alpha.1, introducing a static AdyenCheckout API, return-based callbacks, and a generic <AdyenComponent> view to replace per-method view managers. The review feedback highlights critical areas for improvement, including memory leaks in AdyenComponentViewManager and CheckoutFragment due to missing cleanup on view unmount or fragment dismissal. Additionally, potential runtime crashes were identified from unsafe non-null assertions in CardConfigurationParser and direct appCompatActivity access in ContextModule. Finally, adding cancellation handlers to the suspended continuations in ComponentManager is recommended to prevent illegal state exceptions.

Comment on lines 36 to 41
public override fun createViewInstance(context: ThemedReactContext): DynamicComponentView {
val view = DynamicComponentView(context)
val state = CardViewState(context, messageBusEmitter)
val state = AdyenComponentViewState(context, messageBusEmitter)
view.layoutListener = state
viewStates[view] = state
return view

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The AdyenComponentViewManager maintains a viewStates map to track the state of each DynamicComponentView. However, it does not override onDropViewInstance to clean up these entries when a view is unmounted and destroyed. This leads to a memory leak where both DynamicComponentView and AdyenComponentViewState instances are retained indefinitely. Additionally, the associated native registries (ComponentModule and ContextModule) are never unregistered, which can cause stale state issues.

Suggested change
public override fun createViewInstance(context: ThemedReactContext): DynamicComponentView {
val view = DynamicComponentView(context)
val state = CardViewState(context, messageBusEmitter)
val state = AdyenComponentViewState(context, messageBusEmitter)
view.layoutListener = state
viewStates[view] = state
return view
public override fun createViewInstance(context: ThemedReactContext): DynamicComponentView {
val view = DynamicComponentView(context)
val state = AdyenComponentViewState(context, messageBusEmitter)
view.layoutListener = state
viewStates[view] = state
return view
}
override fun onDropViewInstance(view: DynamicComponentView) {
super.onDropViewInstance(view)
viewStates.remove(view)?.dispose(view)
}

Comment on lines +142 to +153
onSubmit = { data ->
suspendCancellableCoroutine { continuation ->
submitContinuation = continuation
messageBus.onSubmit(data)
}
},
onAdditionalDetails = { data ->
suspendCancellableCoroutine { continuation ->
additionalDetailsContinuation = continuation
messageBus.onAdditionalDetails(data)
}
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The CancellableContinuation instances (submitContinuation and additionalDetailsContinuation) are stored as properties on ComponentManager. If the coroutine scope is cancelled, these continuations are cancelled but the references are not cleared. Trying to resume them later in dispose() or retry() will throw an IllegalStateException. Registering a cancellation handler using invokeOnCancellation to null out the references when the coroutine is cancelled prevents this.

      onSubmit = { data ->
        suspendCancellableCoroutine { continuation ->
          submitContinuation = continuation
          continuation.invokeOnCancellation {
            if (submitContinuation == continuation) {
              submitContinuation = null
            }
          }
          messageBus.onSubmit(data)
        }
      },
      onAdditionalDetails = { data ->
        suspendCancellableCoroutine { continuation ->
          additionalDetailsContinuation = continuation
          continuation.invokeOnCancellation {
            if (additionalDetailsContinuation == continuation) {
              additionalDetailsContinuation = null
            }
          }
          messageBus.onAdditionalDetails(data)
        }
      },

Comment on lines +102 to +106
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
val fragmentTag = tag ?: return
configs[fragmentTag]?.onCancelled?.invoke()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The configs map in CheckoutFragment stores the configuration for each fragment tag. When the fragment is cancelled or dismissed by the user, the entry is never removed from configs. This leads to a memory leak because the FragmentConfig captures references to controllerProvider and onCancelled lambdas. Overriding onDismiss to remove the tag from configs ensures proper cleanup.

  override fun onCancel(dialog: DialogInterface) {
    super.onCancel(dialog)
    val fragmentTag = tag ?: return
    configs[fragmentTag]?.onCancelled?.invoke()
  }

  override fun onDismiss(dialog: DialogInterface) {
    super.onDismiss(dialog)
    tag?.let { configs.remove(it) }
  }

Comment on lines +107 to 120
internal val billingAddressMode: BillingAddressMode?
get() {
return when {
config.hasKey(ADDRESS_VISIBILITY_KEY) -> {
val value = config.getString(ADDRESS_VISIBILITY_KEY)!!
when (value.lowercase()) {
"postal_code", "postal", "postalcode" -> PostalCode()
"full" -> FullAddress(countryCode, supportedCountries.orEmpty())
"lookup" -> AddressConfiguration.Lookup()
else -> None
"postal_code", "postal", "postalcode" -> BillingAddressMode.PostalCode()

"none" -> BillingAddressMode.None()

// TODO: v6 migration - "lookup" and "full" billing address modes not yet available
else -> null
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using the non-null assertion operator !! on config.getString(ADDRESS_VISIBILITY_KEY) is unsafe. In React Native, a key can exist in the ReadableMap but have a null value (i.e., config.isNull(key) is true). If the property is passed as null from JavaScript, this will throw a NullPointerException and crash the app. Use a safe call or retrieve the value defensively to prevent crashes.

  internal val billingAddressMode: BillingAddressMode?
    get() {
      val value = if (config.hasKey(ADDRESS_VISIBILITY_KEY)) config.getString(ADDRESS_VISIBILITY_KEY) else null
      return when (value?.lowercase()) {
        "postal_code", "postal", "postalcode" -> BillingAddressMode.PostalCode()
        "none" -> BillingAddressMode.None()
        else -> null
      }
    }

Comment on lines +263 to +271
fun setup(
sessionModelJSON: ReadableMap,
configurationJSON: ReadableMap,
promise: Promise,
) {
appCompatActivity.lifecycleScope.launch {
setupSessionAsync(sessionModelJSON, configurationJSON, promise)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Accessing appCompatActivity directly inside the @ReactMethod entry points (setup and setupAdvanced) is unsafe. If the React Native application context's current activity is null (e.g., during background transitions or early initialization), appCompatActivity will throw an IllegalStateException and crash the app. It is safer to check if the activity is available first and reject the promise gracefully.

  fun setup(
    sessionModelJSON: ReadableMap,
    configurationJSON: ReadableMap,
    promise: Promise,
  ) {
    val activity = reactApplicationContext.currentActivity as? AppCompatActivity
    if (activity == null) {
      promise.reject(ModuleException.NoActivity())
      return
    }
    activity.lifecycleScope.launch {
      setupSessionAsync(sessionModelJSON, configurationJSON, promise)
    }
  }

…meworks

@testable import Adyen never works against BUILD_LIBRARY_FOR_DISTRIBUTION=YES: that flag only emits a textual .swiftinterface, and @testable needs the binary .swiftmodule format. Confirmed empirically - adding SWIFT_ENABLE_TESTABILITY alongside it still fails; only dropping it produces a real .swiftmodule.

build-xcframeworks.sh gains a TESTABLE=1 mode (no library evolution) for AdyenExampleTests to consume - mutually exclusive with the distribution build per xcframework. build_ios_frameworks.yml builds both via a two-leg matrix, sharing one resolve-package-graph job (cached by Package.swift's hash, since deps change less often than adyen-ios itself) via -clonedSourcePackagesDirPath. Each variant is still cached and consumed independently by its callers.

Also fixes EncodablePaymentComponentDataTests.swift: missing AdyenCard/AdyenComponents/AdyenEncryption imports (now separate xcframeworks, not one umbrella module), and a stale assertion expecting checkoutAttemptId - production code stopped emitting it earlier this migration but the test was never updated.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
28.5% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@descorp descorp changed the title Feat/v6 alpha example testing Feat/v6 alpha Sep 22, 2026
@descorp descorp changed the title Feat/v6 alpha v6 Alpha.1 Sep 22, 2026

This branch has not been deployed

No deployments
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.

1 participant