From a679b783415a1bd5658d181c0b79550e0474d1c1 Mon Sep 17 00:00:00 2001 From: Braxton Ward Date: Thu, 14 May 2026 12:24:20 -0600 Subject: [PATCH] refactor(actions): manage actions called from Atomic.transact --- .../TransactReactNativeModule.kt | 59 +------------ example/App.tsx | 10 +-- ...esentActionScreen.tsx => ActionScreen.tsx} | 58 +++++++++--- example/screens/HomeScreen.tsx | 8 +- ios/TransactReactNative.m | 8 -- ios/TransactReactNative.swift | 54 ++---------- src/android.tsx | 39 +------- src/index.tsx | 56 ++---------- src/ios.tsx | 88 ++----------------- 9 files changed, 79 insertions(+), 301 deletions(-) rename example/screens/{PresentActionScreen.tsx => ActionScreen.tsx} (88%) diff --git a/android/src/main/java/com/atomicfi/transactreactnative/TransactReactNativeModule.kt b/android/src/main/java/com/atomicfi/transactreactnative/TransactReactNativeModule.kt index 93e3bf1..97d63b5 100644 --- a/android/src/main/java/com/atomicfi/transactreactnative/TransactReactNativeModule.kt +++ b/android/src/main/java/com/atomicfi/transactreactnative/TransactReactNativeModule.kt @@ -5,7 +5,6 @@ import android.util.Base64 import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule import financial.atomic.transact.Config -import financial.atomic.transact.ActionConfig import financial.atomic.transact.Transact import financial.atomic.transact.receiver.TransactBroadcastReceiver import org.json.JSONObject @@ -96,6 +95,10 @@ class TransactReactNativeModule(reactContext: ReactApplicationContext) : handleCallbackEvent("onFinish", data, "taskId", emitter, promise) } + override fun onLaunch() { + emitter.emit("onLaunch", null) + } + override fun onInteraction(data: JSONObject) { emitter.emit("onInteraction", data.toString()) } @@ -120,60 +123,6 @@ class TransactReactNativeModule(reactContext: ReactApplicationContext) : } } - @ReactMethod - fun presentAction( - id: String, - environment: ReadableMap, - wrapperVersion: String, - headless: Boolean?, - promise: Promise, - ) { - val context = reactApplicationContext.currentActivity as Context - val emitter = reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - val environmentURL = parseEnvironment(environment) - - try { - val config = ActionConfig( - id = id, - environment = Config.Environment.CUSTOM, - environmentURL = environmentURL, - headless = headless, - ) - config.platform = Config.Platform.suffixed("react-$wrapperVersion") - - Transact.presentAction(context, config) - - val receiver = object : TransactBroadcastReceiver() { - override fun onClose(data: JSONObject) { - handleCallbackEvent("onClose", data, "reason", emitter, promise) - } - - override fun onFinish(data: JSONObject) { - handleCallbackEvent("onFinish", data, "taskId", emitter, promise) - } - - override fun onLaunch() { - emitter.emit("onLaunch", null) - } - - override fun onAuthStatusUpdate(data: JSONObject) { - emitter.emit("onAuthStatusUpdate", data.toString()) - } - - override fun onTaskStatusUpdate(data: JSONObject) { - if (!data.has("failReason")) { - data.put("failReason", JSONObject.NULL) - } - emitter.emit("onTaskStatusUpdate", data.toString()) - } - } - - Transact.registerReceiver(context, receiver) - } catch (e: Exception) { - promise.reject(e) - } - } - companion object { const val NAME = "TransactReactNative" } diff --git a/example/App.tsx b/example/App.tsx index c1a3709..6512a1e 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -3,12 +3,12 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import HomeScreen from './screens/HomeScreen'; import TransactScreen from './screens/TransactScreen'; -import PresentActionScreen from './screens/PresentActionScreen'; +import ActionScreen from './screens/ActionScreen'; export type RootStackParamList = { Home: undefined; Transact: undefined; - PresentAction: undefined; + Action: undefined; }; const Stack = createNativeStackNavigator(); @@ -40,9 +40,9 @@ export default function App() { options={{ title: 'Transact Demo' }} /> diff --git a/example/screens/PresentActionScreen.tsx b/example/screens/ActionScreen.tsx similarity index 88% rename from example/screens/PresentActionScreen.tsx rename to example/screens/ActionScreen.tsx index c4aa298..96d6fec 100644 --- a/example/screens/PresentActionScreen.tsx +++ b/example/screens/ActionScreen.tsx @@ -16,14 +16,16 @@ import { Atomic, Environment, PresentationStyles, + Scope, } from '@atomicfi/transact-react-native'; import type { PresentationStyleIOS } from '@atomicfi/transact-react-native'; -type Props = NativeStackScreenProps; +type Props = NativeStackScreenProps; type EnvironmentOption = 'sandbox' | 'production' | 'custom'; -const PresentActionScreen: React.FC = () => { +const ActionScreen: React.FC = () => { + const [publicToken, setPublicToken] = useState(''); const [actionId, setActionId] = useState(''); const [selectedEnvironment, setSelectedEnvironment] = useState('sandbox'); @@ -65,7 +67,12 @@ const PresentActionScreen: React.FC = () => { } }; - const presentAction = () => { + const launchAction = () => { + if (!publicToken.trim()) { + Alert.alert('Error', 'Please enter a valid public token'); + return; + } + if (!actionId.trim()) { Alert.alert('Error', 'Please enter a valid action ID'); return; @@ -84,12 +91,21 @@ const PresentActionScreen: React.FC = () => { setIsLoading(true); - Atomic.presentAction({ - id: actionId.trim(), + Atomic.transact({ + config: { + publicToken: publicToken.trim(), + scope: Scope.PAYLINK, + tasks: [ + { + operation: 'action', + action: { id: actionId.trim() }, + headless, + }, + ], + }, environment: getEnvironment(), presentationStyleIOS, setDebug: debugEnabled, - headless, onLaunch: () => { console.log('Action launched'); setIsLoading(false); @@ -114,15 +130,28 @@ const PresentActionScreen: React.FC = () => { return ( - Present Action + Action - Launch specific Atomic actions by ID + Launch a specific Atomic action through transact() Configuration + + Public Token * + + + Action ID * = () => { onChangeText={setActionId} placeholder="Enter action ID (e.g., action-123)" placeholderTextColor="#9ca3af" + autoCapitalize="none" + autoCorrect={false} /> @@ -213,8 +244,9 @@ const PresentActionScreen: React.FC = () => { How it works - Present Action allows you to launch specific Atomic actions by their - ID. This is useful for: + Actions are launched through Atomic.transact() with a task whose + operation is "action". The action ID is supplied on the + task and the public token in the config. Use this for: • Launching pre-configured flows @@ -258,11 +290,11 @@ const PresentActionScreen: React.FC = () => { - {isLoading ? 'Launching...' : 'Present Action'} + {isLoading ? 'Launching...' : 'Launch Action'} @@ -441,4 +473,4 @@ const styles = StyleSheet.create({ }, }); -export default PresentActionScreen; +export default ActionScreen; diff --git a/example/screens/HomeScreen.tsx b/example/screens/HomeScreen.tsx index beccf84..9217275 100644 --- a/example/screens/HomeScreen.tsx +++ b/example/screens/HomeScreen.tsx @@ -19,7 +19,7 @@ const HomeScreen: React.FC = ({ navigation }) => { 'This example app demonstrates the integration of the @atomicfi/transact-react-native package.\n\n' + 'Features:\n' + '• Transact Flow - Complete financial connection flow\n' + - '• Present Action - Launch specific actions\n' + + '• Action - Launch a specific action through transact()\n' + '• Environment switching (Sandbox/Production)\n' + '• Custom theming and configuration\n\n' + 'Note: You need valid Atomic credentials to test the actual flows.', @@ -47,11 +47,11 @@ const HomeScreen: React.FC = ({ navigation }) => { navigation.navigate('PresentAction')} + onPress={() => navigation.navigate('Action')} > - Present Action + Launch Action - Launch specific actions by ID + Launch a specific action through transact() diff --git a/ios/TransactReactNative.m b/ios/TransactReactNative.m index 53fbc99..9e74b3c 100644 --- a/ios/TransactReactNative.m +++ b/ios/TransactReactNative.m @@ -12,14 +12,6 @@ @interface RCT_EXTERN_MODULE(TransactReactNative, RCTEventEmitter) withResolver:(RCTPromiseResolveBlock)resolve withRejecter:(RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(presentAction:(NSString *)id - environment:(NSDictionary *)environment - presentationStyle:(nullable NSString *)presentationStyle - setDebug:(nullable NSNumber *)setDebug - headless:(nullable NSNumber *)headless - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) - RCT_EXTERN_METHOD(resolveDataRequest:(id)data) RCT_EXTERN_METHOD(hideTransact:(RCTPromiseResolveBlock)resolve diff --git a/ios/TransactReactNative.swift b/ios/TransactReactNative.swift index dde83c0..a7391e5 100644 --- a/ios/TransactReactNative.swift +++ b/ios/TransactReactNative.swift @@ -101,6 +101,9 @@ class TransactReactNative: RCTEventEmitter { onTaskStatusUpdate: { status in self.sendEvent(withName: "onTaskStatusUpdate", body: status.serialize()) }, + onLaunch: { + self.sendEvent(withName: "onLaunch", body: []) + }, onCompletion: { result in switch result { case .finished(let response): @@ -120,66 +123,19 @@ class TransactReactNative: RCTEventEmitter { } } } - + // Method to receive response from React Native @objc(resolveDataRequest:) func resolveDataRequest(data: Any) -> Void { // Call the stored response handler with the data from React Native if let handler = dataResponseHandler { handler(data) - + // Clear the handler dataResponseHandler = nil } } - - @objc(presentAction:environment:presentationStyle:setDebug:headless:withResolver:withRejecter:) - func presentAction(id: String, environment: [String: Any], presentationStyle: String?, setDebug: NSNumber?, headless: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { - let debugEnabled = setDebug?.boolValue ?? false - let headlessEnabled = headless?.boolValue ?? false - Task { @MainActor in - await Atomic.setDebug(isEnabled: debugEnabled, forwardLogs: { logMessage in - self.sendEvent(withName: "onDebugLog", body: ["message": logMessage]) - }) - - guard let source = RCTPresentedViewController() else { return } - - let parsedEnvironment = self.parseEnvironment(environment) - let parsedPresentationStyle = self.parsePresentationStyle(presentationStyle) - - Atomic.presentAction( - from: source, - id: id, - environment: parsedEnvironment, - presentationStyle: parsedPresentationStyle, - headless: headlessEnabled, - onLaunch: { - self.sendEvent(withName: "onLaunch", body: []) - }, - onAuthStatusUpdate: { status in - self.sendEvent(withName: "onAuthStatusUpdate", body: status.serialize()) - }, - onTaskStatusUpdate: { status in - self.sendEvent(withName: "onTaskStatusUpdate", body: status.serialize()) - }, - onCompletion: { result in - switch result { - case .finished(let response): - resolve(["finished": response.data]) - case .closed(let response): - resolve(["closed": response.data]) - case .error: - resolve(["error": "Unknown error"]) - default: - print("default") - resolve(["error": "Unknown error"]) - } - } - ) - } - } - @objc(hideTransact:withRejecter:) func hideTransact(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { DispatchQueue.main.async { diff --git a/src/android.tsx b/src/android.tsx index e69ce7d..2692f38 100644 --- a/src/android.tsx +++ b/src/android.tsx @@ -21,6 +21,7 @@ export const AtomicAndroid = { environment, wrapperVersion, onInteraction, + onLaunch, onTaskStatusUpdate, onAuthStatusUpdate, onFinish, @@ -32,6 +33,7 @@ export const AtomicAndroid = { environment?: CONSTANTS.TransactEnvironment; wrapperVersion: string; onInteraction?: Function; + onLaunch?: Function; onTaskStatusUpdate?: Function; onAuthStatusUpdate?: Function; onDataRequest?: Function; @@ -40,6 +42,7 @@ export const AtomicAndroid = { }): void { _addEventListener('onClose', onClose); _addEventListener('onFinish', onFinish); + _addEventListener('onLaunch', onLaunch); _addEventListener('onInteraction', onInteraction); _addEventListener('onDataRequest', onDataRequest); _addEventListener('onTaskStatusUpdate', onTaskStatusUpdate); @@ -47,40 +50,4 @@ export const AtomicAndroid = { TransactReactNative.presentTransact(config, environment, wrapperVersion); }, - presentAction({ - TransactReactNative, - id, - environment, - wrapperVersion, - headless, - onLaunch, - onFinish, - onClose, - onTaskStatusUpdate, - onAuthStatusUpdate, - }: { - TransactReactNative: any; - id: String; - environment?: CONSTANTS.TransactEnvironment; - wrapperVersion: string; - headless?: boolean; - onLaunch?: Function; - onFinish?: Function; - onClose?: Function; - onTaskStatusUpdate?: Function; - onAuthStatusUpdate?: Function; - }): void { - _addEventListener('onLaunch', onLaunch); - _addEventListener('onClose', onClose); - _addEventListener('onFinish', onFinish); - _addEventListener('onTaskStatusUpdate', onTaskStatusUpdate); - _addEventListener('onAuthStatusUpdate', onAuthStatusUpdate); - - TransactReactNative.presentAction( - id, - environment, - wrapperVersion, - headless - ); - }, }; diff --git a/src/index.tsx b/src/index.tsx index c7c033d..7a5c2e4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -37,10 +37,12 @@ interface Theme { interface Task { product?: String; // Deprecated - operation: String; + operation?: String; distribution?: Object; navigationOptions?: Object; apps?: AppType[]; + action?: { id: String }; + headless?: boolean; } interface Customer { @@ -99,6 +101,7 @@ export const Atomic = { config, environment, onInteraction, + onLaunch, onFinish, onDataRequest, onClose, @@ -113,6 +116,7 @@ export const Atomic = { onDataRequest?: Function; onAuthStatusUpdate?: Function; onTaskStatusUpdate?: Function; + onLaunch?: Function; onFinish?: Function; onClose?: Function; presentationStyleIOS?: PresentationStyleIOS; @@ -131,6 +135,7 @@ export const Atomic = { environment: environment || CONSTANTS.Environment.production, wrapperVersion, onInteraction, + onLaunch, onFinish, onDataRequest, onClose, @@ -151,55 +156,6 @@ export const Atomic = { throw new Error(`Unsupported OS: ${Platform.OS}`); } }, - presentAction({ - id, - environment, - presentationStyleIOS, - headless, - onLaunch, - onFinish, - onClose, - onAuthStatusUpdate, - onTaskStatusUpdate, - setDebug, - }: { - id: String; - environment?: CONSTANTS.TransactEnvironment; - presentationStyleIOS?: PresentationStyleIOS; - headless?: boolean; - onLaunch?: Function; - onFinish?: Function; - onClose?: Function; - onAuthStatusUpdate?: Function; - onTaskStatusUpdate?: Function; - setDebug?: boolean; - }): void { - const args = { - TransactReactNative, - id, - environment: environment || CONSTANTS.Environment.production, - wrapperVersion, - presentationStyleIOS, - headless, - onLaunch, - onFinish, - onClose, - onAuthStatusUpdate, - onTaskStatusUpdate, - setDebug, - }; - - switch (Platform.OS) { - case 'ios': - AtomicIOS.presentAction(args); - break; - case 'android': - AtomicAndroid.presentAction(args); - break; - default: - throw new Error(`Unsupported OS: ${Platform.OS}`); - } - }, hideTransact() { switch (Platform.OS) { case 'ios': diff --git a/src/ios.tsx b/src/ios.tsx index acee49a..1d7a676 100644 --- a/src/ios.tsx +++ b/src/ios.tsx @@ -8,6 +8,7 @@ export const AtomicIOS = { environment, wrapperVersion, onInteraction, + onLaunch, onFinish, onDataRequest, onClose, @@ -22,6 +23,7 @@ export const AtomicIOS = { wrapperVersion: string; onInteraction?: Function; onDataRequest?: Function; + onLaunch?: Function; onFinish?: Function; onClose?: Function; onAuthStatusUpdate?: Function; @@ -34,12 +36,14 @@ export const AtomicIOS = { ); let onInteractionListener: any; let onDataRequestListener: any; + let onLaunchListener: any; let onAuthStatusUpdateListener: any; let onDebugLogListener: any; const removeListeners = () => { if (onInteractionListener) onInteractionListener.remove(); if (onDataRequestListener) onDataRequestListener.remove(); + if (onLaunchListener) onLaunchListener.remove(); if (onAuthStatusUpdateListener) onAuthStatusUpdateListener.remove(); if (onDebugLogListener) onDebugLogListener.remove(); }; @@ -80,84 +84,6 @@ export const AtomicIOS = { ); } - if (onAuthStatusUpdate) { - onAuthStatusUpdateListener = TransactReactNativeEvents.addListener( - 'onAuthStatusUpdate', - (authStatus) => onAuthStatusUpdate(authStatus) - ); - } - - if (onTaskStatusUpdate) { - TransactReactNativeEvents.addListener( - 'onTaskStatusUpdate', - (taskStatus) => onTaskStatusUpdate(taskStatus) - ); - } - - TransactReactNative.presentTransact( - config, - environment, - presentationStyleIOS, - setDebug, - wrapperVersion - ).then((event: any) => { - if (event.finished && onFinish) { - removeListeners(); - onFinish(event.finished); - } else if (event.closed && onClose) { - removeListeners(); - onClose(event.closed); - } - }); - }, - presentAction({ - TransactReactNative, - id, - environment, - presentationStyleIOS, - headless, - onLaunch, - onFinish, - onClose, - onAuthStatusUpdate, - onTaskStatusUpdate, - setDebug, - }: { - TransactReactNative: any; - id: String; - environment?: CONSTANTS.TransactEnvironment; - // iOS `presentAction` does not yet accept a platform suffix; accepted here - // for parity with Android callers but not forwarded to native. - wrapperVersion?: string; - presentationStyleIOS?: CONSTANTS.PresentationStyleIOS; - headless?: boolean; - onLaunch?: Function; - onFinish?: Function; - onClose?: Function; - onAuthStatusUpdate?: Function; - onTaskStatusUpdate?: Function; - setDebug?: boolean; - }): void { - const TransactReactNativeEvents = new NativeEventEmitter( - TransactReactNative - ); - let onLaunchListener: any; - let onAuthStatusUpdateListener: any; - let onDebugLogListener: any; - - const removeListeners = () => { - if (onLaunchListener) onLaunchListener.remove(); - if (onAuthStatusUpdateListener) onAuthStatusUpdateListener.remove(); - if (onDebugLogListener) onDebugLogListener.remove(); - }; - - onDebugLogListener = TransactReactNativeEvents.addListener( - 'onDebugLog', - (log) => { - console.debug('[TransactNative]', log.message); - } - ); - if (onLaunch) { onLaunchListener = TransactReactNativeEvents.addListener('onLaunch', () => onLaunch() @@ -178,12 +104,12 @@ export const AtomicIOS = { ); } - TransactReactNative.presentAction( - id, + TransactReactNative.presentTransact( + config, environment, presentationStyleIOS, setDebug, - headless + wrapperVersion ).then((event: any) => { if (event.finished && onFinish) { removeListeners();