Skip to main content
Version: 3.9.x.x RR (Android, iOS, Flutter)/ 3.10.x.x RR (React Native)

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

Do not retain operation objects

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:

Bad object re-use example
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).

Bad username example
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()

✅ Do

We recommend to always use the username provided by LocalData.accounts java, swift, objc, flutter, react native.

Correct username example
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()

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.

Bad error handling example
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()

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

Correct error handling example
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
}
}

Implementation Classes

❌ Don't

Never use or rely on SDK-internal implementation classes (suffixed with Impl) as they can change without notice.

Bad type check example
void handleError(MobileAuthenticationClientError error) {
if (error.runtimeType.toString() == "AuthenticationFidoErrorImpl") {
// 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.

Correct type check example
void handleError(MobileAuthenticationClientError error) {
if (error is 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();