Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
Expand All @@ -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"
}
Expand Down
10 changes: 5 additions & 5 deletions example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<RootStackParamList>();
Expand Down Expand Up @@ -40,9 +40,9 @@ export default function App() {
options={{ title: 'Transact Demo' }}
/>
<Stack.Screen
name="PresentAction"
component={PresentActionScreen}
options={{ title: 'Present Action Demo' }}
name="Action"
component={ActionScreen}
options={{ title: 'Action Demo' }}
/>
</Stack.Navigator>
</NavigationContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RootStackParamList, 'PresentAction'>;
type Props = NativeStackScreenProps<RootStackParamList, 'Action'>;

type EnvironmentOption = 'sandbox' | 'production' | 'custom';

const PresentActionScreen: React.FC<Props> = () => {
const ActionScreen: React.FC<Props> = () => {
const [publicToken, setPublicToken] = useState('');
const [actionId, setActionId] = useState('');
const [selectedEnvironment, setSelectedEnvironment] =
useState<EnvironmentOption>('sandbox');
Expand Down Expand Up @@ -65,7 +67,12 @@ const PresentActionScreen: React.FC<Props> = () => {
}
};

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;
Expand All @@ -84,12 +91,21 @@ const PresentActionScreen: React.FC<Props> = () => {

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);
Expand All @@ -114,15 +130,28 @@ const PresentActionScreen: React.FC<Props> = () => {
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Present Action</Text>
<Text style={styles.headerTitle}>Action</Text>
<Text style={styles.headerSubtitle}>
Launch specific Atomic actions by ID
Launch a specific Atomic action through transact()
</Text>
</View>

<View style={styles.section}>
<Text style={styles.sectionTitle}>Configuration</Text>

<View style={styles.inputGroup}>
<Text style={styles.label}>Public Token *</Text>
<TextInput
style={styles.input}
value={publicToken}
onChangeText={setPublicToken}
placeholder="Enter your public token"
placeholderTextColor="#9ca3af"
autoCapitalize="none"
autoCorrect={false}
/>
</View>

<View style={styles.inputGroup}>
<Text style={styles.label}>Action ID *</Text>
<TextInput
Expand All @@ -131,6 +160,8 @@ const PresentActionScreen: React.FC<Props> = () => {
onChangeText={setActionId}
placeholder="Enter action ID (e.g., action-123)"
placeholderTextColor="#9ca3af"
autoCapitalize="none"
autoCorrect={false}
/>
</View>

Expand Down Expand Up @@ -213,8 +244,9 @@ const PresentActionScreen: React.FC<Props> = () => {
<Text style={styles.sectionTitle}>How it works</Text>
<View style={styles.infoCard}>
<Text style={styles.infoText}>
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 &quot;action&quot;. The action ID is supplied on the
task and the public token in the config. Use this for:
</Text>
<Text style={styles.bulletPoint}>
• Launching pre-configured flows
Expand Down Expand Up @@ -258,11 +290,11 @@ const PresentActionScreen: React.FC<Props> = () => {
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.launchButton, isLoading && styles.disabledButton]}
onPress={presentAction}
onPress={launchAction}
disabled={isLoading}
>
<Text style={styles.launchButtonText}>
{isLoading ? 'Launching...' : 'Present Action'}
{isLoading ? 'Launching...' : 'Launch Action'}
</Text>
</TouchableOpacity>
</View>
Expand Down Expand Up @@ -441,4 +473,4 @@ const styles = StyleSheet.create({
},
});

export default PresentActionScreen;
export default ActionScreen;
8 changes: 4 additions & 4 deletions example/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const HomeScreen: React.FC<Props> = ({ 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.',
Expand Down Expand Up @@ -47,11 +47,11 @@ const HomeScreen: React.FC<Props> = ({ navigation }) => {

<TouchableOpacity
style={[styles.button, styles.secondaryButton]}
onPress={() => navigation.navigate('PresentAction')}
onPress={() => navigation.navigate('Action')}
>
<Text style={styles.buttonText}>Present Action</Text>
<Text style={styles.buttonText}>Launch Action</Text>
<Text style={styles.buttonSubtext}>
Launch specific actions by ID
Launch a specific action through transact()
</Text>
</TouchableOpacity>

Expand Down
8 changes: 0 additions & 8 deletions ios/TransactReactNative.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 5 additions & 49 deletions ios/TransactReactNative.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 {
Expand Down
Loading