Skip to content

Mobile Abstraction Layer

Treezor provides a React abstraction layer that lives above the Mobile SDK.

This wrapper allows you to implement SCA features more easily than via the SDK directly.

Warning icon

Alert – The abstraction layer is provided "as is"

Contrary to the SDK, this abstraction layer is provided "as is" without any warranty.

To use it you have to:

  • Use React Native version 0.68 to 0.70 (this can be checked using npx react-native doctor)
  • Import react-native-treezor-sca using credentials provided by your Account Manager.
  • Import bridge-reactNative-{version} which has been provided to you encrypted using GPG.

Using npm install -S {package} or yarn add {package}.

iOS SDK Configuration

Paperclip icon

Prerequisites – Requires Apple Developer Account

You need a valid Apple Developer Account to achieve a valid configuration, as well as an up-to-date membership in Apple Developer Program (see Apple Membership).

You need the following Apple Capabilities to work with the SDK:

  • Push Notifications – Required for SCA Wallet activation (silent push) and for authentication request (classic push)
  • App Groups – Required to decrypt an authentication request and present decrypted content to iOS Push System.

If you're unfamiliar with how to configure these capabilities, you can either refer to the 2.3 section of the SDK documentation or go through the configuration steps listed below.

Configuration steps

In your Apple Developer Portal, you'll need to:

  1. Create your identifier (if it's not already the case)
  2. Configure App Groups
  3. Configure Apple Push Notifications

To create your identifier, go to Identifiers and select or create your application identifier (for a creation, check your identifier has an "Explicit" Bundle ID).

To configure App Groups:

  • Select Identifiers in the left menu of the Developer, then select App Groups from the upper right dropdown
  • Click on the "+" button and follow the instructions from the creation assistant
  • Open your Application Identifier and click "Configure" from the "App Groups" feature. Then, select your previously created App Groups Identifier.

To Configure Push Notifications:

  • Select Keys in the left menu of the Developer Portal.
  • Click on the "+" button and:
    • Provide a "Key name"
    • Check "Apple Push Notifications service (APNs)"
    • Click on "Continue", then "Register"
  • Click on the "Download" button to retrieve the generated key and the "APNS Key ID" value. Also keep the "Team ID" value.
  • Open your Application Identifier and click "Configure" from the "Push Notification" feature.

You can also:

  • Follow the Apple's documentation to add an Apple Push Notification Production SSL certificate.
  • Send Push information to the SDK

AntelopRelease.plist

Using XCode, create AntelopRelease.plist file in ios -> AppName -> AntelopRelease.plist and add it to your TargetAppName.

Your file should look like this:

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<dict>
		<key>fr.antelop.alertLoggingEnabled</key>
		<string>The Antelop SDK is logging.</string>
		<key>fr.antelop.applicationGroupIdentifier</key>
		<string>group.com.treezor.scaintegration</string>
		<key>fr.antelop.initialConnectionTimeout</key>
		<integer>60</integer>
		<key>fr.antelop.application_id</key>
		<integer>{yourApplicationId}</integer>
		<key>fr.antelop.issuer_id</key>
		<string>{yourIssuerId}</string>
		<key>fr.antelop.teamIdentifier</key>
		<string>{yourTeamIdentifier}</string>
	</dict>
</plist>

Configure XCode

If you are using the last architecture of React Native which uses Obj-c++, you will need to add the following flag: Target->BuildSettings->Other C++ flags: -fmodules and -fcxx-modules

If you get the "C++ modules are disabled" error, please set the following settings to true. Target->BuildSettings->Enable Modules(C and Objective-C)

Configure AppDelegate

In AppDelegate.mm, add @import react_native_antelop; at the top of the document.

c
@import react_native_antelop;

Override the AppDelegate.didFinishLaunchingWithOptions and use this code.

c
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // [...] Your application code
    // Add these lines at end of the method  
    [AntelopBridgeAppDelegate.shared didFinishLaunchingWithOptionsWithApplication:application launchOptions:launchOptions];
  return YES;
}
Note icon

Note – Do not replace your complete didFinishLaunchingWithOptions method

Just add the provided code at the end of the method.

Override the AppDelegate.applicationDidBecomeActive and use this code :

c
- (void)applicationDidBecomeActive:(UIApplication *)application {
  [AntelopBridgeAppDelegate.shared applicationDidBecomeActiveWithApplication:application];
}

Override the AppDelegate.didRegisterForRemoteNotificationsWithDeviceToken and use this code:

c
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  // You can retreive the deviceToken for you own Server side push
  // Link to Antelop Bridge
  [AntelopBridgeAppDelegate.shared didRegisterForRemoteNotificationsWithDeviceTokenWithApplication:application data:deviceToken];
}

Override the AppDelegate.didReceiveRemoteNotification and use this code:

c
-(void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void
(^)(UIBackgroundFetchResult))completionHandler
{
  typedef void (^FetchCompletionHandler)(UIBackgroundFetchResult);
  typedef void (^ClientNotificationHandler)(NSDictionary * _Nonnull, FetchCompletionHandler);
  ClientNotificationHandler clientNotificationHandler = ^void(NSDictionary * _Nonnull userInfo, FetchCompletionHandler handler) {
    // To be implemented if the default behaviour is not desired
  };
  [AntelopBridgeAppDelegate.shared didReceiveRemoteNotificationWithUserInfo:userInfo completionHandler:completionHandler clientNotificationHandler:clientNotificationHandler];
}

Override the AppDelegate.performFetchWithCompletionHandler and use this code:

c
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  [AntelopBridgeAppDelegate.shared performFetchWithCompletionHandlerWithApplication:application completion:completionHandler];
}

Push notifications

Adding the Notification Service is required to decrypt the encrypted content of the Push Notifications.

To add the necessary files for iOS, run the command yarn installNotificationIOS

Note icon

Note – installNotificationIOS script depends on npm xcode package

Therefore, you should make sure that your package.json specifies this package (yarn add xcode or npm i xcode).

This will add a new target "PushExtension" to your project. Verify the settings below for the newly added target:

  • In XCode navigate to section "General"
  • Verify your Bundle identifier, Version and Build
  • Navigate to section "Signing & Capabilities"
  • Add "App Groups" by clicking "+ Capability" and check the group identifier embed in your provisioning profile
  • Verify your Bundle identifier
  • Navigate to section "Build Settings":
  • Search for "SWIFT_VERSION" in the filter and ensure that it is valued to "SWIFT 5"

In order to share data with the main app, the Extension must have access to the App Group declared in the AntelopRelease.plist at fr.antelop.applicationGroupIdentifier.

In your Podfile, add these lines to the target of your application:

c
pod 'react-native-antelop', :path => '../node_modules/react-native-antelop'
pod 'AntelopSDK', :path => '../node_modules/react-native-antelop/ios/Frameworks/'
pod 'SecureCModule', :path => '../node_modules/react-native-antelop/ios/Frameworks/'

In your Podfile, add these lines at the end of the file:

c
target 'PushExtension' do
	config = use_native_modules!
	use_react_native!(:path => config["reactNativePath"])

	inherit! :search_paths
	post_install do |installer|
		installer.pods_project.targets.each do |target|
		target.build_configurations.each do |config|
			config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'
		end
		end
		flipper_post_install(installer)
	end
end

Make sure to add the AntelopRelease.plist to the Extension Target: File Inspector -> Target Membership, check the box for the Extension Target if necessary.

Android SDK Configuration

Metadata

Add the following metadata to android/app/src/main/AndroidManifest.xml

xml
<meta-data
  android:name="fr.antelop.application_id"
  android:value="\4713640103500149457" />
<meta-data
  android:name="fr.antelop.issuer_id"
  android:value="treezor" />

Permissions

Add the following permissions to android/app/src/main/AndroidManifest.xml

xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />

Libraries

Add the following libraries to android/build.gradle

json
allprojects {
  // ...
  repositories {
    // ...
    flatDir {
      dirs project(':react-native-antelop').file('libs')
      dirs project(':react-native-antelop').file('google-libs')
    }
  }
  // ...
}

Add the following to android/app/build.gradle (if not already present)

json
dependencies {
  // ...
  implementation 'androidx.work:work-runtime-ktx:2.7.0'
}
books icon

Reading – More SDK configuration information

If you need more information regarding initial configuration of the SDK on iOS or Android, contact Treezor to request access to the SDK full documentation.

Usage

Declare a custom prompt YourCustomPrompt.tsx using:

js
import {
  StrongCustomerAuthenticationProvider,
  CustomPromptProps,
  CustomPromptAction,
  CustomPromptError,
} from 'react-native-treezor-sca';

const YourCustomPrompt = ({
  onPasscodeEntered,
  onClose,
  action,
  error,
}: CustomPromptProps) => {
  const title: Record<CustomPromptAction, string> = {
    auth: 'Enter your passcode',
    check: 'Enter your current passcode',
    setup: isConfirmation ? 'Repeat your passcode' : 'Set your passcode',
    update: isConfirmation
      ? 'Repeat your new passcode'
      : 'Enter your new passcode',
    unlock: 'Enter your passcode to unlock your account',
    activateBiometric: 'Enter your passcode to activate biometric',
    deactivateBiometric: 'Enter your passcode to deactivate biometric',
  };

  const scaErrors: Record<CustomPromptError, string> = {
    wrongPasscode: remainingPasscodeAttempts
      ? `Invalid passcode.\n You have ${remainingPasscodeAttempts} attempt(s) left`
      : 'Invalid passcode.\n Please try again ',
    wrongBiometrics:
      "Many incorrect attempts to verify biometrics; use passcode until it's reactivated again",
  };

  return (
    <Modal>
      <Text>{title[action]}</Text>
      {error && <Text>{errors[error]}</Text>}

      {/* Rest of your prompt design & logic */}
    </Modal>
  );
};

When a passcode error in the prompt occurs, the set remainingPasscodeAttempts value depends on the context.

Passcode error contextValueDescription
SCAnumericalThe number of remaining attempts before the authentication passcode is blocked. Applies to both per-session and per-operation SCA.
SCA Wallet actionundefinedSpecifically for biometry activation/deactivation or unlocking of the SCA Wallet. If there are too many erroneous attempts, the SCA Wallet will be blocked.

Wrap your application in App.tsx using the following.

js
import { StrongCustomerAuthenticationProvider } from 'react-native-treezor-sca';

return (
	<StrongCustomerAuthenticationProvider Prompt={YourCustomPrompt}>
	{/* Your application */}
	</StrongCustomerAuthenticationProvider>
);

Then initialize the abstraction layer using:

js
import { useStrongCustomerAuthentication } from 'react-native-treezor-sca';

const {
  isConnected,
  biometricStatus,
  destroy,
  connect,
  disconnect,
  provision,
  sign,
  changePasscode,
  activateBiometric,
  deactivateBiometric,
} = useStrongCustomerAuthentication({
  rootedDeviceForbidden: false,
  onError: (code, details) => console.error(code, details),
  onProvisioningRequired: async () => {
    const scaWallet = await MyApiService.createScaWallet({ userId });
    provision(scaWallet.activationCode);
  },
  onChangeCredentialsSuccess: () => console.info('Passcode has been updated'),
  onCheckCredentialsSuccess: () => console.info('Passcode has been checked'),
  onActivateBiometricSuccess: () =>
    console.info('Biometric Authentication has been activated'),
  onDeactivateBiometricSuccess: () =>
    console.info('Biometric Authentication has been deactivated'),
});

Connect to an SCA Wallet

js
// by calling the connect() method :
// if no wallet exists, onProvisioningRequired() will be called
// if a wallet is already provisionned, a simple connexion to it is established
connect();

You should only connect to an SCA Wallet once the End User has authenticated on your app.

Generate an SCA Proof (without user action)

js
// Generate a JWS to make session calls
const signature = await sign();
const decoded = jwtDecode(signature);
console.info(decoded);

Generate an SCA Proof (with user action)

Generate an SCA proof with a user passcode or user biometrics request.

js
// Generate a JWS to make session calls
const signature = await sign({ type: 'authenticate' });
const decoded = jwtDecode(signature);
console.info(decoded);

Customize prompt biometric

js
// Customizing biometric prompt with android title, android subtitle or ios subtitle
const signature = await sign({
  type: 'authenticate',
  biometryMessage: { 
    androidTitle: 'Android title to custom', 
    androidSubTitle: 'Android subtitle to custom', 
    iosSubTitle: "iOS subtitle to custom 
  },
});

Sign parameters interface

js
// Sign params interface
const sign = async (options: ISignOptions = { type: 'none' })

interface ISignOptions {
  type: ISignType; // authentication type
  data?: unknown; // data to be displayed as part of the authentication or null
  biometryMessage?: IBiometryMessage; // custom biometric prompt message
}

interface IBiometryMessage {
  title?: string;
  subTitle?: string;
}

Sign act parameters interface

js
// Sign act params interface
const signAct = (
  operation: OperationSignature, // url and body of the sign act
  biometryMessage?: IBiometryMessage // you can also customize biometric prompt message for signAct also
)

export type OperationSignature = {
  url: string;
  body: unknown;
};

Activate biometric

js
// Activate biometric
activateBiometric();

Deactivate biometric

js
// Deactivate biometric
deactivateBiometric();

Generate a per-operation SCA Proof

js
// Generate a JWS to update a Card's limits, with a user passcode request
const signature = await signAct({
  url: '{baseUrl}/v1/cards/{cardId}/Limits/',
  body: {
    limitAtmMonth: 1000,
    limitPaymentMonth: 800,
  },
});
const decoded = jwtDecode(signature);
console.info(decoded);

You can check out the format of the payload to sign.

Change the passcode

js
// change passcode requested by end user
changePasscode();

Disconnect from an SCA Wallet

js
// disconnect from an SCA Wallet
disconnect();

A disconnect should occur when your app is put to the background, or when the End User disconnects from your app.

Delete an SCA Wallet

js
// irreversibly destroy the SCA Wallet from mobile device
destroy();

Listeners

Subscribing for "WalletEventListener" and "PushAuthenticationRequestListener" enables the application to capture events from remote notifications, thereby ensuring correct wallet and authentication statuses. For subscription, a set of callback functions for each event have to be provided.

Find below two examples of listening to notifications, with display or actions triggered afterwards.

Example 1 – Listen and display log or toaster outside the application

js
import {AppRegistry} from 'react-native';
import {Toast} from 'react-native-toast-message/lib/src/Toast';
import {
  WalletEventListener,
  PushAuthenticationRequestListener,
} from 'react-native-antelop';
import {name as appName} from './app.json';
import App from './src/App';

const walletEventListenerProtocole = {
  onWalletLoaded: () => {
    Toast.show({
      type: 'info',
      text1: 'Wallet has been loaded',
    });
    console.log('Event', '@WalletEventListener/onWalletLoaded');
  },
  onWalletLocked: reason => {
    Toast.show({
      type: 'info',
      text1: 'Wallet has been locked',
    });
    console.log('Event', '@WalletEventListener/onWalletLocked', reason);
  },
  onWalletUnlocked: () => {
    Toast.show({
      type: 'info',
      text1: 'Wallet has been unlocked',
    });
    console.log('Event', '@WalletEventListener/onWalletUnlocked');
  },
  onWalletDeleted: () => {
    Toast.show({
      type: 'info',
      text1: 'Wallet has been deleted',
    });
    console.log('Event', '@WalletEventListener/onWalletDeleted');
  },
  onLogout: () => {
    console.log('Event', '@WalletEventListener/onLogout');
  },
  onSettingsUpdated: () => {
    console.log('Event', '@WalletEventListener/onSettingsUpdated');
  },
  onCountersUpdated: () => {
    console.log('Event', '@WalletEventListener/onCountersUpdated');
  },
  onCustomerCredentialsReset: () => {
    console.log('Event', '@WalletEventListener/onCustomerCredentialsReset');
  },
  onLostEligibility: () => {
    console.log('Event', '@WalletEventListener/onLostEligibility');
  },
  onCardsUpdated: () => {
    console.log('Event', '@WalletEventListener/onCardsUpdated');
  },
};

const pushAuthProtocole = {
  onRequestReceived(pushAuthenticationRequestId) {
    console.log(
      'PushAuthenticationRequestFetcher',
      'onRequestReceived',
      pushAuthenticationRequestId,
    );
  },
  onRequestCancelled(requestId, reason) {
    console.log(
      'PushAuthenticationRequestFetcher',
      'onRequestCancelled',
      requestId,
      reason,
    );
  },
};

PushAuthenticationRequestListener().setBackgroundListener(pushAuthProtocole);
PushAuthenticationRequestListener().setForegroundListener(pushAuthProtocole);

WalletEventListener().setBackgroundListener(walletEventListenerProtocole);
WalletEventListener().setForegroundListener(walletEventListenerProtocole);

AppRegistry.registerComponent(appName, () => App);

Example 2 – Listen and trigger appropriate actions inside the application

In this example, we listen for notification events relating to the SCA Wallet status (e.g., onWalletLocked, onCredentialsReset, and onWalletDelete) to dispatch an event from the index.js (other notifications are handled as the same way as the first example).

Inside the application logic (MobileActions.tsx), on capturing the event, we display a modal with explanation and a CTA button to disconnect the SCA Wallet.

index.js

js
import {AppRegistry, NativeEventEmitter, NativeModules} from 'react-native';
import {Toast} from 'react-native-toast-message/lib/src/Toast';
import {
  WalletEventListener,
  PushAuthenticationRequestListener,
} from 'react-native-antelop';
import {name as appName} from './app.json';
import App from './src/App';

// For ios, the EmitterModule must be declared beforehand as describled in: https://reactnative.dev/docs/native-modules-ios
// As example, Add RCTEmitterModule.h and RCTEmitterModule.m inside the ios folder to to register the emitter module
const {EmitterModule} = NativeModules;
const eventEmitter = new NativeEventEmitter(EmitterModule);

const walletEventListenerProtocole = {
  onWalletLoaded: () => {
    Toast.show({
      type: 'info',
      text1: 'Wallet has been loaded',
    });
    console.log('Event', '@WalletEventListener/onWalletLoaded');
  },
  onWalletLocked: reason => {
    console.log('Event', '@WalletEventListener/onWalletLocked', reason);
    // dispatch an event in case of WalletLocked
    eventEmitter.emit('onWalletUpdated', {type: 'Wallet locked', reason});
  },
  onWalletUnlocked: () => {
    Toast.show({
      type: 'info',
      text1: 'Wallet has been unlocked',
    });
    console.log('Event', '@WalletEventListener/onWalletUnlocked');
  },
  onWalletDeleted: () => {
    console.log('Event', '@WalletEventListener/onWalletDeleted');
    // dispatch an event in case of WalletDeleted
    eventEmitter.emit('onWalletUpdated', {type: 'Wallet deleted'});
  },
  onLogout: () => {
    console.log('Event', '@WalletEventListener/onLogout');
  },
  onSettingsUpdated: () => {
    console.log('Event', '@WalletEventListener/onSettingsUpdated');
  },
  onCountersUpdated: () => {
    console.log('Event', '@WalletEventListener/onCountersUpdated');
  },
  onCustomerCredentialsReset: () => {
    console.log('Event', '@WalletEventListener/onCustomerCredentialsReset');
    // dispatch an event in case of CustomerCredentialsReset
    eventEmitter.emit('onWalletUpdated', {type: 'Credentials reset'});
  },
  onLostEligibility: () => {
    console.log('Event', '@WalletEventListener/onLostEligibility');
  },
  onCardsUpdated: () => {
    console.log('Event', '@WalletEventListener/onCardsUpdated');
  },
};

const pushAuthProtocole = {
  onRequestReceived(pushAuthenticationRequestId) {
    console.log(
      'PushAuthenticationRequestFetcher',
      'onRequestReceived',
      pushAuthenticationRequestId,
    );
  },
  onRequestCancelled(requestId, reason) {
    console.log(
      'PushAuthenticationRequestFetcher',
      'onRequestCancelled',
      requestId,
      reason,
    );
  },
};

PushAuthenticationRequestListener().setBackgroundListener(pushAuthProtocole);
PushAuthenticationRequestListener().setForegroundListener(pushAuthProtocole);

WalletEventListener().setBackgroundListener(walletEventListenerProtocole);
WalletEventListener().setForegroundListener(walletEventListenerProtocole);

AppRegistry.registerComponent(appName, () => App);

MobileActions.tsx

js
...
const [walletStatus, setWalletStatus] = useState<string | undefined>();
...
// Listening to onWalletUpdated to display the modal on received
// Remove listener on unmount
useEffect(() => {
	eventEmitter.addListener('onWalletUpdated', data => {
		console.log(data);
		isConnected && setWalletStatus(data?.type);
	});

	return () => {
		eventEmitter.removeAllListeners('onWalletUpdated');
	};
}, [isConnected]);
...
// Display the modal with explanation and CTA button to disconnect the sca wallet
<>
	{!!walletStatus && (
		<ModalView
			title={walletStatus}
			description={
				'Your wallet has been updated. Please disconnect then re-connect again to the SCA wallet'
			}
			onCloseModal={() => setWalletStatus(undefined)}
			onSubmit={() => {
				setWalletStatus(undefined);
				disconnect();
			}}
		/>
	)}
</>

Errors

Treezor abstraction layer returns error objects in the following format:

json
{
 "code": 449,
 "error": "Canceled"
}

Here is the list of errors you may encounter:

HTTP Status codeSDK errorDescription
401Invalid CredentialsOccurs when the passcode of the end user are invalid or provided in an invalid format.
404Wallet Not FoundOccurs when the SCA Wallet doesn't exist (deleted, wrong Id).
405Passcode Authentication ImpossibleOccurs when the number of authentication attempts with a passcode is reached. To unblock this situation, you may activate the biometry or reset the passcode (via the app, dashboard, or your own back office with a PIN reset).
406Device Not EligibleOccurs when the device is not supported (due to the OS version for instance).
407Authentication ErrorOccurs when there is an issue with the strong customer authentication (timeout, duplicate, etc.).
416Wallet Not ActivatedOccurs when the SCA Wallet is not active.
423Wallet LockedOccurs when the SCA Wallet is locked. You may unlock the SCA Wallet per your end user's request in the Dashboard or in your own back office.
428Activation RequiredOccurs when the end user is active on their device and an action is made from the back office or Dashboard (unlock or reset PIN). The end user must log out and log back in order to reactivate their account or reinitialize their passwords.
451Other ErrorContact Treezor for more information.
498Invalid Activation CodeOccurs when the activation code is invalid, already used, expired or locked during the SCA Wallet initialization or provisioning.
499CanceledOccurs when the end user cancels the SCA.
500Internal ErrorOccurs in case of an unexpected server error. Contact Treezor if the error persists.
511Network Not AvailableOccurs when there is a network issue.