import React from 'react'; import { EmitterSubscription, NativeEventEmitter, NativeModules, requireNativeComponent, StyleSheet, View, } from 'react-native'; import { MFCurrencyISO, MFV3PaymentMode } from './MFEnums'; import { MFV3GetSessionData, MFV3PaymentResult, MFV3SessionData, MFV3VerifyResult, MFV3WidgetEvent, MFCardViewStyle, MFError, } from './MFModels'; import { modelParser } from './MFUtils'; //#region V3 Card View (MFCardV3Module) // Fall back to a no-op so `new NativeEventEmitter(...)` never throws at import even // if the native module isn't linked. Method calls will surface a clear error. const MFCardV3Native = NativeModules.MFCardV3Module ?? { addListener: () => {}, removeListeners: () => {} }; const MFCardV3Emitter = new NativeEventEmitter(MFCardV3Native); const CardViewV3 = requireNativeComponent('MFCardViewV3'); const MFCardV3Constants = { CardResizedEventName: 'onMFV3CardResized', WidgetEventName: 'onMFV3WidgetEvent', ErrorEventName: 'onMFV3Error', }; const styles = StyleSheet.create({ container: { width: '100%' }, }); interface IMFCardV3NativeProps { style?: any; paymentStyle?: MFCardViewStyle; cardId?: string; } // Unique id per mounted MFCardViewV3 instance. The native view registers itself // under this id so the module's promise methods can reach the exact instance — // works identically across old/new RN architecture and (in future) Android, // without relying on findNodeHandle / UIManager view resolution. let _nextCardId = 0; const genCardId = (): string => `mfcardv3-${++_nextCardId}`; export interface IMFCardViewV3Props { /** Width/positioning styles for the card view container. Height is driven automatically. */ style?: any; /** Reused MFCardViewStyle object. `ShowCardholderName` is V3-only. */ paymentStyle?: MFCardViewStyle; /** Fired whenever the widget's content height changes (the view resizes itself too). */ onHeightChanged?: (height: number) => void; /** Fired for subscribed widget lifecycle events (VIEW_READY, CARD_IDENTIFIED, ...). */ onWidgetEvent?: (event: MFV3WidgetEvent) => void; /** Fired if the widget fails to load (e.g. a session id missing its country prefix). */ onError?: (error: MFError) => void; } interface IMFCardViewV3State { height: number; } /** * The V3 embedded card view. Renders a native, per-instance card widget (no singleton). * Usage: * const ref = useRef(null); * const session = await MFV3Session.create({ amount: 1, currency: MFCurrencyISO.KUWAIT_KWD }); * await ref.current.load(session); * const result = await ref.current.submit(MFCurrencyISO.KUWAIT_KWD); */ export class MFCardViewV3 extends React.Component { private cardId = genCardId(); private subscriptions: EmitterSubscription[] = []; constructor(props: IMFCardViewV3Props) { super(props); this.state = { height: 0 }; } componentDidMount() { this.subscriptions.push( MFCardV3Emitter.addListener(MFCardV3Constants.CardResizedEventName, (payload: { cardId: string; height: number }) => { if (!this.isForThisView(payload?.cardId)) return; const height = payload.height; if (height > 1) this.setState({ height }); this.props.onHeightChanged && this.props.onHeightChanged(height); }) ); this.subscriptions.push( MFCardV3Emitter.addListener(MFCardV3Constants.WidgetEventName, (payload: { cardId: string } & MFV3WidgetEvent) => { if (!this.isForThisView(payload?.cardId)) return; this.props.onWidgetEvent && this.props.onWidgetEvent({ name: payload.name, id: payload.id, paymentMethodName: payload.paymentMethodName }); }) ); this.subscriptions.push( MFCardV3Emitter.addListener(MFCardV3Constants.ErrorEventName, (payload: { cardId: string } & MFError) => { if (!this.isForThisView(payload?.cardId)) return; this.props.onError && this.props.onError({ code: payload.code, message: payload.message } as MFError); }) ); } componentWillUnmount() { this.subscriptions.forEach((s) => s.remove()); this.subscriptions = []; } private isForThisView(cardId?: string): boolean { return cardId != null && cardId === this.cardId; } /** Renders the card widget for the given session. Call before submit/verify. */ async load(session: MFV3SessionData): Promise { await MFCardV3Native.load(this.cardId, session); } /** Submits the entered card (pay / collect, per the session mode). */ async submit(currency?: MFCurrencyISO): Promise { const jsonResponse = await MFCardV3Native.submit(this.cardId, currency ?? ''); return modelParser(jsonResponse); } /** Verifies the entered card (VERIFY session — no charge). */ async verify(): Promise { const jsonResponse = await MFCardV3Native.verify(this.cardId); return modelParser(jsonResponse); } render() { const { style, paymentStyle } = this.props; return ( 0 ? { height: this.state.height } : null]}> ); } } //#endregion //#region Session API export interface IMFV3CreateSessionRequest { amount: number; currency?: MFCurrencyISO; mode?: MFV3PaymentMode; require3DS?: boolean; saveCard?: boolean; customerReference?: string; } export interface IMFV3CreateVerifySessionRequest { customerReference: string; currency?: MFCurrencyISO; require3DS?: boolean; } /** * Session helpers. Two integration paths: * - SDK path: create / createVerify use the API key set via MFSDK.init. * - Backend path: your server creates the session; pass the id to fromBackend. */ export const MFV3Session = { async create(request: IMFV3CreateSessionRequest): Promise { const jsonResponse = await MFCardV3Native.create({ Amount: request.amount, Currency: request.currency ?? null, Mode: request.mode ?? MFV3PaymentMode.COMPLETE_PAYMENT, Require3DS: request.require3DS ?? true, SaveCard: request.saveCard ?? false, CustomerReference: request.customerReference ?? null, }); return modelParser(jsonResponse); }, async createVerify(request: IMFV3CreateVerifySessionRequest): Promise { const jsonResponse = await MFCardV3Native.createVerify({ CustomerReference: request.customerReference, Currency: request.currency ?? null, Require3DS: request.require3DS ?? true, }); return modelParser(jsonResponse); }, /** * Backend path: build a session from an id your server created via POST /v3/sessions. * SessionId must be the full country-prefixed id (e.g. "KWT-abc123"). Pass * encryptionKey too if you want the SDK to decrypt the inline (non-3DS) result. */ fromBackend(sessionId: string, encryptionKey?: string): MFV3SessionData { return { SessionId: sessionId, EncryptionKey: encryptionKey }; }, }; //#endregion // Re-export the request types so the whole V3 API can be imported from one place. export type { MFV3PaymentResult, MFV3VerifyResult, MFV3SessionData, MFV3GetSessionData, MFV3WidgetEvent };