Best Practices
This section focuses on our recommended best practices when using our SDKs and cross-platform (Flutter and React Native) plugins. They are based on our own experiences and customer feedback.
SDK / Flutter and React Native plugin releases
We are frequently updating our native SDKs and cross-platform plugins with new features, bug fixes, security improvements and ensuring mobile OS compatibility.
❌ Don't
Stay on old SDK or cross-platform plugin releases for a prolonged amount of time. Not only you are missing out on new features, bug fixes and security improvements, but you are also running into the potential issue of losing mobile OS compatibility. Nevis provides full support for the current release and maintenance support for the prior one.
✅ Do
- Frequently check our release notes for Android, iOS, React Native and Flutter.
- Plan and schedule frequent updates of your app to stay up-to-date with our releases.
Operations in Flutter and React Native plugins
The Flutter and React Native plugins use operation caches to synchronise operations and their callbacks between the native and cross-platform layers.
❌ Don't
It is very important to be aware of this fact because, as a consequence, operation objects cannot be re-used, as they are removed after their lifecycle ended. Retaining and re-using operation objects in the cross-platform plugins will lead to crashes.
Here is a "bad example" of retaining the OutOfBandAuthentication
object to explain operation object reuse. This example is of course valid for all operations and although shown in React Native also applies to Flutter:
- React Native/TypeScript
let auth: OutOfBandAuthentication; //the problem starts here
client.operations.outOfBandOperation
.payload(payload)
.onRegistration(async (registration) => {
// handle registration
})
.onAuthentication(async (authentication) => {
if (auth === undefined) {
auth = authentication; // this is the problematic part; you are not always using the new authentication object, that is provided by the SDK plugin but assiging a "stale" already used object
}
// it will cause problems here as not the `authentication` object is used but the potentially stale `auth` object
await auth.accountSelector(this.accountSelector)
.authenticatorSelector(this.authenticatorSelector)
.pinUserVerifier(this.pinUserVerifier)
.biometricUserVerifier(this.biometricUserVerifier)
.onSuccess((authorizationProvider?: AuthorizationProvider) => {
// handle success and use AuthorizationProvider if needed
})
.onError((error: OperationError) => {
// handle the error
})
.execute();
})
.onError((error: OutOfBandOperationError) => {
// handle the error
})
.execute();
✅ Do
Refer to our example apps for guidance of the correct usage of our SDK.
Username handling
The username used in authentication, deregistration, change PIN and change password operations is the technical user identifier stored in the Account.username()
java, swift, objc, flutter, react native property.
❌ Don't
Do not use the login identifier (e.g., the user`s email address).
- Android/Kotlin
- Android/Java
- iOS/Swift
- Flutter/Dart
- React Native/TypeScript
val loginId = "user@example.com"
client.operations().authentication()
.username(loginId)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess { authorizationProvider: AuthorizationProvider ->
// handle success and use AuthorizationProvider if needed
}
.onError { error: AuthenticationError ->
// handle the error
}
.execute()
String loginId = "user@example.com";
client.operations().authentication()
.username(loginId)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess(authorizationProvider -> {
// handle success and use AuthorizationProvider if needed
})
.onError(error -> {
// handle the error
})
.execute();
let loginId = "user@example.com"
client.operations.authentication
.username(loginId)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess { authorizationProvider in
// handle success and use AuthorizationProvider if needed
}
.onError { error in
// handle the error
}
.execute()
final loginId = "user@example.com";
await client.operations.authentication
.username(loginId)
.authenticatorSelector(_authenticatorSelector)
.pinUserVerifier(_pinUserVerifier)
.biometricUserVerifier(_biometricUserVerifier)
.onSuccess((AuthorizationProvider? authorizationProvider) {
// handle success and use AuthorizationProvider if needed
})
.onError((AuthenticationError error) {
// handle the error
})
.execute();
const loginId = 'user@example.com';
await client.operations.authentication
.username(loginId)
.authenticatorSelector(this.authenticatorSelector)
.pinUserVerifier(this.pinUserVerifier)
.biometricUserVerifier(this.biometricUserVerifier)
.onSuccess((authorizationProvider?: AuthorizationProvider) => {
// handle success and use AuthorizationProvider if needed
})
.onError((error: AuthenticationError) => {
// handle the error
})
.execute();
✅ Do
We recommend to always use the username provided by LocalData.accounts
java, swift, objc, flutter, react native.
- Android/Kotlin
- Android/Java
- iOS/Swift
- Flutter/Dart
- React Native/TypeScript
val accounts = client.localData.accounts()
val account = accounts.first() // implement proper account selection in your own production app
client.operations().authentication()
.username(account.username())
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess { authorizationProvider: AuthorizationProvider ->
// handle success and use AuthorizationProvider if needed
}
.onError { error: AuthenticationError ->
// handle the error
}
.execute()
Set<Account> accounts = client.localData().accounts();
Account account = accounts.iterator().next(); // implement proper account selection in your own production app
client.operations().authentication()
.username(account.username())
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess(authorizationProvider -> {
// handle success and use AuthorizationProvider if needed
})
.onError(error -> {
// handle the error
})
.execute();
let accounts = client.localData.accounts
let account = accounts.first; // implement proper account selection in your own production app
client.operations.authentication
.username(account.username)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess { authorizationProvider in
// handle success and use AuthorizationProvider if needed
}
.onError { error in
// handle the error
}
.execute()
final accounts = await client.localData.accounts;
final account = accounts.first; // implement proper account selection in your own production app
await client.operations.authentication
.username(account.username)
.authenticatorSelector(_authenticatorSelector)
.pinUserVerifier(_pinUserVerifier)
.biometricUserVerifier(_biometricUserVerifier)
.onSuccess((AuthorizationProvider? authorizationProvider) {
// handle success and use AuthorizationProvider if needed
})
.onError((AuthenticationError error) {
// handle the error
})
.execute();
const accounts = await client.localData.accounts();
const account = accounts[0]; // implement proper account selection in your own production app
await client.operations.authentication
.username(account.username)
.authenticatorSelector(this.authenticatorSelector)
.pinUserVerifier(this.pinUserVerifier)
.biometricUserVerifier(this.biometricUserVerifier)
.onSuccess((authorizationProvider?: AuthorizationProvider) => {
// handle success and use AuthorizationProvider if needed
})
.onError((error: AuthenticationError) => {
// handle the error
})
.execute();
Error handling related to end-users
See the Error Handling chapter for more documentation related to the different errors and error types. Our example apps directly show the technical error occurring. Be aware that this is not to be considered best practice.
❌ Don't
Never directly show the description()
java, flutter, react native method/field or the message
property (on iOS) of an error to your end users. It is not a localized message and is targeted to developers in the context of debugging/problem analysis.
- Android/Kotlin
- Android/Java
- iOS/Swift
- Flutter/Dart
- React Native/TypeScript
client.operations().authentication()
.username(username)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess { authorizationProvider: AuthorizationProvider ->
// handle success and use AuthorizationProvider if needed
}
.onError { error: AuthenticationError ->
Toast.makeText(context, error.description(), Toast.LENGTH_LONG).show()
}
.execute()
client.operations().authentication()
.username(username)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess(authorizationProvider -> {
// handle success and use AuthorizationProvider if needed
})
.onError(error -> {
Toast.makeText(context, error.description(), Toast.LENGTH_LONG).show();
})
.execute();
client.operations.authentication
.username(username)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess { authorizationProvider in
// handle success and use AuthorizationProvider if needed
}
.onError { error in
let alert = UIAlertController(title: "Authentication Failed",
message: error.localizedDescription,
preferredStyle: .alert)
present(alert, animated: true)
}
.execute()
await client.operations.authentication
.username(username)
.authenticatorSelector(_authenticatorSelector)
.pinUserVerifier(_pinUserVerifier)
.biometricUserVerifier(_biometricUserVerifier)
.onSuccess((AuthorizationProvider? authorizationProvider) {
// handle success and use AuthorizationProvider if needed
})
.onError((AuthenticationError error) {
_showDialog(title: "Authentication Failed", message: error.description);
})
.execute();
await client.operations.authentication
.username(username)
.authenticatorSelector(this.authenticatorSelector)
.pinUserVerifier(this.pinUserVerifier)
.biometricUserVerifier(this.biometricUserVerifier)
.onSuccess((authorizationProvider?: AuthorizationProvider) => {
// handle success and use AuthorizationProvider if needed
})
.onError((error: AuthenticationError) => {
showDialog('Authentication Failed', error.description);
})
.execute();
✅ Do
Your own production app should handle the errors in an appropriate manner such as providing translations for all your supported languages as well as simplifying the error message presented to the end-user in a way non-technical adverse people can understand and act upon them.
- Android/Kotlin
- Android/Java
- iOS/Swift
- Flutter/Dart
- React Native/TypeScript
fun authenticate(username: String) {
client.operations().authentication()
.username(username)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess { authorizationProvider: AuthorizationProvider ->
// handle success and use AuthorizationProvider if needed
}
.onError { error: AuthenticationError ->
// use description only for debugging or problem analysis
Log.e("ERROR", "Authentication failed. Error: " + error.description())
val message = getString(resourceIdForError(error))
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
.execute()
}
fun resourceIdForError(error: MobileAuthenticationClientError): Int {
return when (error) {
is AuthenticationError -> R.string.authentication_error
// add other error cases
else -> R.string.general_error
}
}
void authenticate(String username) {
client.operations().authentication()
.username(username)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess(authorizationProvider -> {
// handle success and use AuthorizationProvider if needed
})
.onError(error -> {
// use description only for debugging or problem analysis
Log.e("ERROR", "Authentication failed. Error: " + error.description());
String message = getResources().getString(resourceIdForError(error));
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
})
.execute();
}
int resourceIdForError(MobileAuthenticationClientError error) {
if (error instanceof AuthenticationError) {
return R.string.authentication_error;
}
// add other error cases
return R.string.general_error;
}
func authenticate(username: String) {
client.operations.authentication
.username(username)
.authenticatorSelector(authenticatorSelector)
.pinUserVerifier(pinUserVerifier)
.biometricUserVerifier(biometricUserVerifier)
.onSuccess { authorizationProvider in
// handle success and use AuthorizationProvider if needed
}
.onError { error in
// use description only for debugging or problem analysis
os_log("Authentication failed. Error: \(error.localizedDescription)")
let message = localized(error: error)
let alert = UIAlertController(title: "Authentication Failed",
message: message,
preferredStyle: .alert)
present(alert, animated: true)
}
.execute()
}
func localized(error: MobileAuthenticationClientError) -> String {
switch error {
case let AuthenticationError.FidoError(errorCode, cause, _):
return NSLocalizedString("authentication_error", comment: "")
// add other error cases
default:
return NSLocalizedString("general_error", comment: "")
}
}
Future<void> authenticate(String username) {
return client.operations.authentication
.username(username)
.authenticatorSelector(_authenticatorSelector)
.pinUserVerifier(_pinUserVerifier)
.biometricUserVerifier(_biometricUserVerifier)
.onSuccess((AuthorizationProvider? authorizationProvider) {
// handle success and use AuthorizationProvider if needed
})
.onError((AuthenticationError error) {
// use description only for debugging or problem analysis
debugPrint("Authentication failed. Error: ${error.description}");
final message = _localizedError(error);
_showDialog(title: "Authentication Failed", message: message);
})
.execute();
}
String _localizedError(MobileAuthenticationClientError error) {
if (error is AuthenticationError) {
return localizations.authenticationError;
}
// add other error cases
return localizations.generalError;
}
async function authenticate(username: String) {
await client.operations.authentication
.username(username)
.authenticatorSelector(this.authenticatorSelector)
.pinUserVerifier(this.pinUserVerifier)
.biometricUserVerifier(this.biometricUserVerifier)
.onSuccess((authorizationProvider?: AuthorizationProvider) => {
// handle success and use AuthorizationProvider if needed
})
.onError((error: AuthenticationError) => {
// use description only for debugging or problem analysis
console.log(`Authentication failed. Error: ${error.description}`);
const message = localizedError(error);
showDialog('Authentication Failed', message);
})
.execute();
}
function localizedError(error: MobileAuthenticationClientError) {
if (error instanceof AuthenticationError) {
return i18next.t('authentication.error');
}
// add other error cases
return i18next.t('general.error');
}
Implementation Classes
❌ Don't
Never use or rely on SDK-internal implementation classes (suffixed with Impl
) as they can change without notice.
- Flutter/Dart
- React Native/TypeScript
void handleError(MobileAuthenticationClientError error) {
if (error.runtimeType.toString() == "AuthenticationFidoErrorImpl") {
// handle error
}
// handle other errors
}
function handleError(error: MobileAuthenticationClientError) {
if (error.constructor.name === 'AuthenticationFidoError') {
// handle error
}
// handle other errors
}
✅ Do
Always use the interfaces, protocols or abstract classes provided by our SDK API in your app logic. In case breaking changes occur in these APIs it will be clearly mentioned in the release notes.
- Flutter/Dart
- React Native/TypeScript
void handleError(MobileAuthenticationClientError error) {
if (error is AuthenticationFidoError) {
// handle error
}
// handle other errors
}
function handleError(error: MobileAuthenticationClientError) {
if (error instanceof AuthenticationFidoError) {
// handle error
}
// handle other errors
}
Object destructuring
❌ Don't
Object destructuring is currently not supported by the React Native plugin. The current implementation relies heavily on using this
inside functions defined in the API classes.
As consequence, if object destructuring is used to extract
a function from any of the API classes, upon the execution of that destructured function, the this
context will be different. This results in the React Native plugin showing unexpected behaviour like errors at runtime.
const { cancelAuthentication } = await handler.listenForOsCredentials();
✅ Do
const authenticationListenHandler = await handler.listenForOsCredentials();
await authenticationListenHandler.cancelAuthentication();