MFA Transaction Signing
Enrol a user, set a PIN, and sign a transaction from Dart, on Android and iOS alike.
Overview
Section titled “Overview”Read MFA Transaction Signing first for the three-call flow and what the token proves.
All three return a typed result rather than throwing. A refusal arrives as a result with
success == false and an errorCode.
Basic Usage
Section titled “Basic Usage”import 'package:alphyn_sdk/alphyn_sdk.dart';
// 1. Once per (user, device), after login. No biometric prompt appears here.final enrolment = await AlphynSdk.enrollForMfa(userId);if (!enrolment.success) { debugPrint('enrol failed: ${enrolment.errorCode}');}
// 2. Set or change the PIN. Verified and hashed ON THE SERVER.await AlphynSdk.setMfaPin(userId: userId, newPin: newPin);await AlphynSdk.setMfaPin(userId: userId, newPin: newPin, oldPin: oldPin);
// 3. Per transaction, after the user clears your confirmation screen.final result = await AlphynSdk.signAction( userId: userId, actionLabel: 'TRANSFER', context: {'amount': '250.00', 'currency': 'TRY', 'toIBAN': 'TR12…'}, pin: pin,);
if (result.success) { await sendToYourBackend(result.token);}Methods
Section titled “Methods”static Future<MfaResult> enrollForMfa(String userId)
static Future<MfaResult> setMfaPin({ required String userId, required String newPin, String? oldPin,})
static Future<ActionVerificationResult> signAction({ required String userId, required String actionLabel, required Map<String, Object?> context, String? pin, MfaSubject? subject,})Parameters
Section titled “Parameters”| Parameter | Type | Notes |
|---|---|---|
userId | String | your identifier for the logged-in user, not one Alphyn issues |
newPin / oldPin | String | omit oldPin on a first set. An empty string is treated as omitted |
actionLabel | String | your own name for the operation, e.g. TRANSFER |
context | Map<String, Object?> | the transaction. Its hash is bound into the signature |
pin | String? | optional. null omits the factor rather than failing |
subject | MfaSubject? | optional. null is byte-identical to declaring nothing |
The PIN crosses as a String because that is what Dart has; the native bridge converts it to bytes
and zeroes its own copy. A Dart String is immutable and cannot be wiped, so do not hold one longer
than the call.
MfaResult
Section titled “MfaResult”success, errorCode, errorMessage, securityLevel, enrollmentId.
errorCode is the backend’s string verbatim. Branch on it, never on errorMessage.
ActionVerificationResult
Section titled “ActionVerificationResult”success, token, expiresAtEpochMs, errorCode, errorMessage.
Declaring an identity
Section titled “Declaring an identity”final result = await AlphynSdk.signAction( userId: userId, actionLabel: 'TRANSFER', context: {'amount': '250.00', 'currency': 'TRY'}, pin: pin, subject: MfaSubject(tc: '10000000146', phone: '+905342223344'),);MfaSubject carries three optional strings, tc, phone and email. The values cross to the
native core, which validates and tokenises them; nothing is hashed in Dart.
A field that does not conform is dropped, never corrected, and a subject in which nothing survived produces a request byte-identical to one that declared none. See Subject identity for the accepted forms and for why a token must be treated as identifying data.
iOS builds and the simulator
Section titled “iOS builds and the simulator”The Alphyn core ships arm64 slices. On an Intel Mac, or with an unpinned xcodebuild destination,
the simulator build will also try to compile x86_64 and fail with unsupported Swift architecture.
Flutter’s generated Xcode configuration is included after the CocoaPods one and overrides the
exclusion the pod declares, so it has to be re-applied in your Podfile:
post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end # Flutter/Generated.xcconfig is included AFTER the Pods xcconfig and re-sets # EXCLUDED_ARCHS[sdk=iphonesimulator*], so the exclusion is applied here, last. installer.aggregate_targets.each do |aggregate| aggregate.user_project.native_targets.each do |target| target.build_configurations.each do |config| config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'i386 x86_64' end end aggregate.user_project.save endendNot needed for device builds, and harmless there.
Use Cases
Section titled “Use Cases”- Signing a transfer, withdrawal or beneficiary change so your backend can prove afterwards that a specific user’s device authorised those exact values
- Adding a second factor to a high-risk action without an SMS round trip
- Correlating a signed transaction with a person across institutions, through a subject token
Example: signing a transfer end to end
Section titled “Example: signing a transfer end to end”Future<void> confirmTransfer(String userId, String iban, String amount, String pin) async { final result = await AlphynSdk.signAction( userId: userId, actionLabel: 'TRANSFER', context: {'amount': amount, 'currency': 'TRY', 'toIBAN': iban}, pin: pin, subject: MfaSubject(tc: customerTckn), );
if (!result.success) { // THREAT_DETECTED is a refusal, not a transport failure. Do not retry silently. showError(result.errorCode); return; }
// Your backend validates this token before it moves anything. await yourApi.transfer(iban, amount, result.token);}The token is evidence, not a decision. Your backend must validate it server-side and compare the echoed context against the transfer it is about to perform; see Backend verification.