Commit 567d3cbb authored by Javokhir's avatar Javokhir
Browse files

feat: add typed MyID JS API over codegen turbo module spec

Public surface is a promise-based start({config, iosAppearance}) mirroring the
MyID Flutter plugin's wire format, with string-union enums, platform-scoped
JSDoc and a MyIdError carrying the SDK result code.

The codegen spec stays deliberately untyped for enums because codegen cannot
express string unions; narrowing lives in the public layer only. Keys that are
undefined are stripped before crossing the bridge so the SDK's own defaults
survive rather than being overwritten with empty values.
parent 9d97b218
import { Text, View, StyleSheet } from 'react-native';
import { multiply } from 'myid-sdk';
import { useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import { isMyIdError, start } from 'myid-sdk';
const result = multiply(3, 7);
// Replace with a sessionId issued by your backend and the hashes from MyID.
const SESSION_ID = '';
const CLIENT_HASH = '';
const CLIENT_HASH_ID = '';
export default function App() {
const [status, setStatus] = useState('Idle');
const onPress = async () => {
setStatus('Starting...');
try {
const result = await start({
config: {
sessionId: SESSION_ID,
clientHash: CLIENT_HASH,
clientHashId: CLIENT_HASH_ID,
environment: 'DEBUG',
},
});
setStatus(`Code ${result.code}`);
} catch (error) {
setStatus(
isMyIdError(error) ? `${error.code}: ${error.message}` : 'Unknown error'
);
}
};
return (
<View style={styles.container}>
<Text>Result: {result}</Text>
<Text style={styles.status}>{status}</Text>
<Button title="Start MyID" onPress={onPress} />
</View>
);
}
......@@ -16,5 +44,9 @@ const styles = StyleSheet.create({
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 16,
},
status: {
fontSize: 16,
},
});
import { TurboModuleRegistry, type TurboModule } from 'react-native';
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
/**
* Codegen spec. This is the raw bridge contract, NOT the public API.
*
* Every enum crosses as a plain `string` because codegen cannot express TS
* string unions — the narrowing lives in `myid-types.ts`, and the native side
* uppercases and falls back to the SDK default for anything it does not know.
*
* Nested object params must be *named* exported type aliases; an inline object
* literal degrades to `AnyTypeAnnotation` and loses type safety on both platforms.
*/
export type MyIdOrganizationDetailsInput = {
phone?: string;
/** Native asset name: Android drawable / iOS image-set. Not a file path or URL. */
logo?: string;
};
export type MyIdConfigInput = {
sessionId: string;
clientHash: string;
clientHashId: string;
minAge?: number;
distance?: number;
residency?: string;
environment?: string;
entryType?: string;
locale?: string;
cameraShape?: string;
cameraSelector?: string;
cameraResolution?: string;
imageFormat?: string;
screenOrientation?: string;
presentationStyle?: string;
withSoundGuides?: boolean;
showErrorScreen?: boolean;
huaweiAppId?: string;
organizationDetails?: MyIdOrganizationDetailsInput;
};
export type MyIdAppearanceInput = {
colorPrimary?: string;
colorOnPrimary?: string;
colorError?: string;
colorOnError?: string;
colorOutline?: string;
colorDivider?: string;
colorSuccess?: string;
colorButtonContainer?: string;
colorButtonContainerDisabled?: string;
colorButtonContent?: string;
colorButtonContentDisabled?: string;
colorScanButtonContainer?: string;
buttonCornerRadius?: number;
};
export type MyIdResultPayload = {
code: string;
base64?: string;
};
export interface Spec extends TurboModule {
multiply(a: number, b: number): number;
/**
* Both params are required at the bridge level — the public wrapper always
* sends an appearance object, passing `{}` when the caller omits it.
*/
start(
config: MyIdConfigInput,
appearance: MyIdAppearanceInput
): Promise<MyIdResultPayload>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('MyidSdk');
export { multiply } from './multiply';
export { start } from './start-myid';
export { MyIdError, MyIdErrorCode, isMyIdError } from './myid-error';
export type { MyIdErrorCodeValue } from './myid-error';
export {
MY_ID_CAMERA_RESOLUTIONS,
MY_ID_CAMERA_SELECTORS,
MY_ID_CAMERA_SHAPES,
MY_ID_ENTRY_TYPES,
MY_ID_ENVIRONMENTS,
MY_ID_IMAGE_FORMATS,
MY_ID_LOCALES,
MY_ID_PRESENTATION_STYLES,
MY_ID_RESIDENCIES,
MY_ID_SCREEN_ORIENTATIONS,
} from './myid-enums';
export type {
MyIdCameraResolution,
MyIdCameraSelector,
MyIdCameraShape,
MyIdEntryType,
MyIdEnvironment,
MyIdImageFormat,
MyIdLocale,
MyIdPresentationStyle,
MyIdResidency,
MyIdScreenOrientation,
} from './myid-enums';
export type {
MyIdAppearance,
MyIdConfig,
MyIdOrganizationDetails,
MyIdResult,
StartMyIdOptions,
} from './myid-types';
import MyidSdk from './NativeMyidSdk';
export function multiply(a: number, b: number): number {
return MyidSdk.multiply(a, b);
}
export function multiply(_a: number, _b: number): number {
throw new Error("'myid-sdk' is only supported on native platforms.");
}
/**
* Wire values for every MyID enum.
*
* These strings are the bridge format and match the MyID Flutter plugin
* exactly, so backend docs and existing integration knowledge transfer
* unchanged. The native layer uppercases the incoming value and falls back to
* the SDK's own default when it does not recognise it.
*/
export type MyIdEnvironment = 'PRODUCTION' | 'DEBUG';
export type MyIdEntryType =
'IDENTIFICATION' | 'VIDEO_IDENTIFICATION' | 'FACE_DETECTION';
export type MyIdResidency = 'USER_DEFINED' | 'RESIDENT' | 'NON_RESIDENT';
export type MyIdLocale =
'UZBEK' | 'UZBEK_CYRILLIC' | 'KARAKALPAK' | 'TAJIK' | 'ENGLISH' | 'RUSSIAN';
export type MyIdCameraShape = 'CIRCLE' | 'ELLIPSE';
export type MyIdCameraSelector = 'FRONT' | 'BACK';
export type MyIdCameraResolution = 'LOW' | 'HIGH';
export type MyIdImageFormat = 'JPEG' | 'PNG';
export type MyIdPresentationStyle = 'SHEET' | 'FULL';
export type MyIdScreenOrientation = 'PORTRAIT' | 'LANDSCAPE' | 'FULL';
export const MY_ID_ENVIRONMENTS = ['PRODUCTION', 'DEBUG'] as const;
export const MY_ID_ENTRY_TYPES = [
'IDENTIFICATION',
'VIDEO_IDENTIFICATION',
'FACE_DETECTION',
] as const;
export const MY_ID_RESIDENCIES = [
'USER_DEFINED',
'RESIDENT',
'NON_RESIDENT',
] as const;
export const MY_ID_LOCALES = [
'UZBEK',
'UZBEK_CYRILLIC',
'KARAKALPAK',
'TAJIK',
'ENGLISH',
'RUSSIAN',
] as const;
export const MY_ID_CAMERA_SHAPES = ['CIRCLE', 'ELLIPSE'] as const;
export const MY_ID_CAMERA_SELECTORS = ['FRONT', 'BACK'] as const;
export const MY_ID_CAMERA_RESOLUTIONS = ['LOW', 'HIGH'] as const;
export const MY_ID_IMAGE_FORMATS = ['JPEG', 'PNG'] as const;
export const MY_ID_PRESENTATION_STYLES = ['SHEET', 'FULL'] as const;
export const MY_ID_SCREEN_ORIENTATIONS = [
'PORTRAIT',
'LANDSCAPE',
'FULL',
] as const;
/**
* Documented MyID result codes.
*
* The SDK can emit codes beyond these (liveness advice codes such as 3, 14,
* 20..28), so never treat this map as exhaustive — always compare against
* `error.code` directly rather than assuming one of these values.
*
* Full list: https://docs.myid.uz/#/ru/embedded?id=javob-kodlar-uz-result_code
*/
export const MyIdErrorCode = {
/** User dismissed the flow. */
USER_CANCELLED: '101',
CAMERA_PERMISSION_DENIED: '102',
/** Generic SDK or server failure, also used for wrapper-level errors. */
FAILURE: '103',
USER_BANNED: '122',
} as const;
export type MyIdErrorCodeValue =
(typeof MyIdErrorCode)[keyof typeof MyIdErrorCode];
/**
* Thrown by `start()` for every failure path, including user cancellation.
*/
export class MyIdError extends Error {
/** SDK result code as a string, e.g. `'101'`. */
readonly code: string;
constructor(code: string, message: string) {
super(message);
this.code = code;
this.name = 'MyIdError';
// Restores the prototype chain, which is lost when a built-in is
// subclassed and the output is down-levelled.
Object.setPrototypeOf(this, MyIdError.prototype);
}
}
export function isMyIdError(error: unknown): error is MyIdError {
return error instanceof MyIdError;
}
import type {
MyIdCameraResolution,
MyIdCameraSelector,
MyIdCameraShape,
MyIdEntryType,
MyIdEnvironment,
MyIdImageFormat,
MyIdLocale,
MyIdPresentationStyle,
MyIdResidency,
MyIdScreenOrientation,
} from './myid-enums';
/**
* Branding shown on the SDK's input and error screens.
*/
export type MyIdOrganizationDetails = {
/**
* Call-centre number shown on the error screen.
* Defaults to MyID's own number (712022202) when omitted.
*/
phone?: string;
/**
* Name of a native image asset — an Android drawable or an iOS image set.
* Not a file path, require() result or URL. The asset must live in the host
* app, and fits an image view of roughly 240x60.
*/
logo?: string;
};
/**
* Configuration for a verification session.
*
* Only `sessionId`, `clientHash` and `clientHashId` are required; every omitted
* field keeps the SDK's own default rather than a default chosen by this
* library. Fields marked `@platform` are silently ignored by the other
* platform.
*/
export type MyIdConfig = {
/** Issued by your backend via the MyID API. Not a client-side value. */
sessionId: string;
/** Provided by the MyID sales team. Treat as a secret. */
clientHash: string;
/** Provided by the MyID sales team. */
clientHashId: string;
/** Minimum age the user must meet. SDK default: 16. */
minAge?: number;
/** Face-match threshold, 0..1. SDK default: 0.65 (Android) / 0.60 (iOS). */
distance?: number;
/**
* When set to `USER_DEFINED` and no passport data is available, the SDK shows
* its own passport-input screen with a resident/non-resident checkbox.
*/
residency?: MyIdResidency;
/** `DEBUG` targets the MyID sandbox. SDK default: `PRODUCTION`. */
environment?: MyIdEnvironment;
/**
* `FACE_DETECTION` only captures a face and skips MyID verification.
* `VIDEO_IDENTIFICATION` is experimental and Android-only — iOS falls back to
* `IDENTIFICATION`.
*/
entryType?: MyIdEntryType;
/**
* SDK default: `UZBEK`.
* `UZBEK_CYRILLIC`, `KARAKALPAK` and `TAJIK` exist on Android only; iOS falls
* back to `UZBEK`.
*/
locale?: MyIdLocale;
/** Shape of the face-capture cutout. SDK default: `CIRCLE`. */
cameraShape?: MyIdCameraShape;
/** SDK default: `FRONT`. */
cameraSelector?: MyIdCameraSelector;
/** @platform android — ignored on iOS. SDK default: `LOW`. */
cameraResolution?: MyIdCameraResolution;
/** @platform android — ignored on iOS. SDK default: `PNG`. */
imageFormat?: MyIdImageFormat;
/** @platform android — ignored on iOS. */
screenOrientation?: MyIdScreenOrientation;
/** @platform ios — ignored on Android. How the SDK is presented modally. */
presentationStyle?: MyIdPresentationStyle;
/** @platform android — ignored on iOS. Audio guidance during capture. */
withSoundGuides?: boolean;
/** Whether the SDK renders its own error screens. SDK default: `true`. */
showErrorScreen?: boolean;
/**
* @platform android — ignored on iOS.
* Required on Huawei devices without Google Play Services; without it the SDK
* can fail to initialise. Obtain it from AppGallery Connect.
*/
huaweiAppId?: string;
organizationDetails?: MyIdOrganizationDetails;
};
/**
* iOS-only theming. Android is themed by overriding the SDK's own XML
* resources (`myid_color_*` in `myid_colors.xml`, `myid_button_corner_radius`
* in `myid_dimens.xml`) — see the README.
*
* Colors accept `#RRGGBB` or `RRGGBB`.
*/
export type MyIdAppearance = {
/** Guides the user through the flow — the SDK's dominant accent color. */
colorPrimary?: string;
/** Text and icons drawn on top of `colorPrimary`. */
colorOnPrimary?: string;
colorError?: string;
/** Text and icons drawn on top of error backgrounds. */
colorOnError?: string;
/** Borders and outlines on inputs and cards. */
colorOutline?: string;
/** Thin lines separating UI sections. */
colorDivider?: string;
colorSuccess?: string;
colorButtonContainer?: string;
colorButtonContainerDisabled?: string;
colorButtonContent?: string;
colorButtonContentDisabled?: string;
/** Background of the scan icon button. */
colorScanButtonContainer?: string;
/** Corner radius of primary buttons. SDK default: 12. */
buttonCornerRadius?: number;
};
export type MyIdResult = {
/** MyID result code. `100` indicates success. */
code: string;
/**
* Captured face portrait as base64-encoded JPEG, without a data-URI prefix.
* Absent when the SDK returned no image.
*/
base64?: string;
};
export type StartMyIdOptions = {
config: MyIdConfig;
/** @platform ios — ignored on Android. */
iosAppearance?: MyIdAppearance;
};
import NativeMyidSdk from './NativeMyidSdk';
import type {
MyIdAppearanceInput,
MyIdConfigInput,
MyIdOrganizationDetailsInput,
} from './NativeMyidSdk';
import { MyIdError, MyIdErrorCode } from './myid-error';
import type { MyIdResult, StartMyIdOptions } from './myid-types';
/**
* Drops keys whose value is `undefined`.
*
* This matters: the native layer only applies a setting when its key is
* present, so an absent key preserves the SDK's own default. Forwarding
* `undefined` would instead push a null through the bridge and could override a
* default with an empty value.
*/
function stripUndefined<T extends object>(value: T): T {
const entries = Object.entries(value).filter(
([, fieldValue]) => fieldValue !== undefined
);
return Object.fromEntries(entries) as T;
}
/**
* Reads the `code` a native rejection carries. React Native surfaces both
* platforms' reject(code, message) as an Error with a `code` property, but it
* arrives as a string on one platform and can be a number on the other.
*/
function readErrorCode(error: unknown): string {
if (typeof error === 'object' && error !== null && 'code' in error) {
const { code } = error as { code: unknown };
if (typeof code === 'string' && code.length > 0) {
return code;
}
if (typeof code === 'number') {
return String(code);
}
}
return MyIdErrorCode.FAILURE;
}
function readErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}
return 'MyID verification failed';
}
/**
* Launches the MyID verification flow and resolves once the user completes it.
*
* Rejects with a {@link MyIdError} for every failure path — including the user
* simply backing out, which arrives as code `101`.
*
* @example
* ```ts
* try {
* const result = await start({
* config: { sessionId, clientHash, clientHashId },
* });
* console.log(result.code, result.base64);
* } catch (error) {
* if (isMyIdError(error) && error.code === MyIdErrorCode.USER_CANCELLED) {
* // user backed out — usually not worth surfacing
* }
* }
* ```
*/
export async function start(options: StartMyIdOptions): Promise<MyIdResult> {
const { config, iosAppearance } = options;
const payload = stripUndefined({
...config,
organizationDetails: config.organizationDetails
? stripUndefined<MyIdOrganizationDetailsInput>(config.organizationDetails)
: undefined,
}) as MyIdConfigInput;
const appearance: MyIdAppearanceInput = iosAppearance
? stripUndefined<MyIdAppearanceInput>(iosAppearance)
: {};
try {
const result = await NativeMyidSdk.start(payload, appearance);
return { code: result.code, base64: result.base64 };
} catch (error) {
throw new MyIdError(readErrorCode(error), readErrorMessage(error));
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment