Skip to content

MFA Transaction Signing

Enrol a user, set a PIN, and sign a transaction from Dart, on Android and iOS alike.

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.

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);
}
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,
})
ParameterTypeNotes
userIdStringyour identifier for the logged-in user, not one Alphyn issues
newPin / oldPinStringomit oldPin on a first set. An empty string is treated as omitted
actionLabelStringyour own name for the operation, e.g. TRANSFER
contextMap<String, Object?>the transaction. Its hash is bound into the signature
pinString?optional. null omits the factor rather than failing
subjectMfaSubject?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.

success, errorCode, errorMessage, securityLevel, enrollmentId.

errorCode is the backend’s string verbatim. Branch on it, never on errorMessage.

success, token, expiresAtEpochMs, errorCode, errorMessage.

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.

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
end
end

Not needed for device builds, and harmless there.

  • 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
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.