Keyri Flutter SDK
Note that the following documentation is focused specifically on Keyri's QR authentication use case. Other use cases as discussed elsewhere in this documentation can be implemented with the Keyri Flutter SDK using the same named methods, but are not covered here, and instead should be referenced in their use case-specific documentation page.
System Requirements
- Android API level 23 or higher
- AndroidX compatibility
- iOS 14+
- Swift 5+
- Apple A7 chip or newer (The A7 shipped with the iPhone 5s)
Integration
To integrate the Keyri Dart Package, run this command in terminal in root of the project directory:
$ flutter pub add keyri_v3
The SDK can then be imported into any dart file as follows:
import 'package:keyri_v3/keyri.dart';
API
The Keyri Flutter API reference can be found on pub.dev: https://pub.dev/documentation/keyri/latest/ (opens in a new tab)
More detailed descriptions of each method can be found on the relevant native SDK documentation pages:
Usage
Below is an example code block which utilizes the Keyri library in 2 common ways:
easyKeyriAuth()
This function allows Keyri to handle everything for you - simply pass in the User Id, App Key, and Payload and Keyri will handle displaying a built-in QR scanner, get the session information, present a Confirmation Dialogue to the user, and finalize the details with the API.
initiateQrSession()
This function can be used if you'd like to present your own custom Scanner and/or Confirmation dialogue to the user. This also works for Universal Links/App Links (see below) Calling this function returns a Session object, which can be handled by:
-
Automatically confirming (via
confirmSession()
) -
Presenting your own custom confirmation screen to the user
-
Presenting the built-in Keyri confirmation screen (
initializeDefaultScreen()
) - this method is used below
import 'package:flutter/material.dart';
import 'package:keyri_v3/keyri.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
void main() {
runApp(const MyApp());
}
const String appKey = "[Your app key here]"; // Change it before launch
const String? publicApiKey = null; // Change it before launch, optional
const String? serviceEncryptionKey = null; // Change it before launch, optional
const bool blockEmulatorDetection = true;
const String? publicUserId = null; // Change it before launch, optional
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Keyri Example',
theme: ThemeData(primarySwatch: Colors.blue),
home: const KeyriHomePage(title: 'Keyri Example'));
}
}
class KeyriHomePage extends StatefulWidget {
const KeyriHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<KeyriHomePage> createState() => _KeyriHomePageState();
}
class _KeyriHomePageState extends State<KeyriHomePage> {
Keyri keyri = Keyri(appKey, publicApiKey: publicApiKey, serviceEncryptionKey: serviceEncryptionKey, blockEmulatorDetection: true);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
button(_easyKeyriAuth, 'Easy Keyri Auth'),
button(_customUI, 'Custom UI')
],
),
),
);
}
void _easyKeyriAuth() {
keyri
.easyKeyriAuth('Some payload', publicUserId: publicUserId)
.then((authResult) => _onAuthResult(authResult == true ? true : false))
.catchError((error, stackTrace) => _onError(error));
}
void _customUI() {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const KeyriScannerAuthPage()));
}
void _onError(String message) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
void _onAuthResult(bool result) {
String text;
if (result) {
text = 'Successfully authenticated!';
} else {
text = 'Authentication failed';
}
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
}
Widget button(VoidCallback onPressedCallback, String text) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.deepPurple,
),
onPressed: onPressedCallback,
child: Text(text),
);
}
}
class KeyriScannerAuthPage extends StatefulWidget {
const KeyriScannerAuthPage({Key? key}) : super(key: key);
@override
State<KeyriScannerAuthPage> createState() => _KeyriScannerAuthPageState();
}
class _KeyriScannerAuthPageState extends State<KeyriScannerAuthPage> {
bool _isLoading = false;
Keyri keyri = Keyri(appKey, publicApiKey: publicApiKey, serviceEncryptionKey: serviceEncryptionKey, blockEmulatorDetection: true);
void onMobileScannerDetect(BarcodeCapture barcodes) {
if (barcodes.barcodes.isNotEmpty && !_isLoading) {
var barcode = barcodes.barcodes[0];
if (barcode.rawValue == null) {
debugPrint('Failed to scan Barcode');
return;
}
final String? code = barcode.rawValue;
debugPrint('Scanned barcode: $code');
if (code == null) return;
var sessionId = Uri.dataFromString(code).queryParameters['sessionId'];
if (sessionId == null) return;
setState(() {
_isLoading = true;
});
_onReadSessionId(sessionId);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Expanded(
flex: 1,
child: _isLoading
? const Center(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [CircularProgressIndicator()]))
: MobileScanner(onDetect: onMobileScannerDetect),
)
],
),
);
}
Future<void> _onReadSessionId(String sessionId) async {
keyri
.initiateQrSession(sessionId, publicUserId: publicUserId)
.then((session) => keyri
.initializeDefaultConfirmationScreen('Some payload')
.then((authResult) => _onAuthResult(authResult))
.catchError((error, stackTrace) => _onError(error.toString())))
.catchError((error, stackTrace) => _onError(error.toString()));
}
void _onError(String message) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
Future.delayed(const Duration(milliseconds: 2000), () {
setState(() {
_isLoading = false;
});
});
}
void _onAuthResult(bool result) {
var successfullyAuthenticatedText = 'Successfully authenticated!';
if (!result) {
successfullyAuthenticatedText = 'Failed to authenticate';
}
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(successfullyAuthenticatedText)));
setState(() {
_isLoading = false;
});
}
}
Deep linking
iOS - Universal Links
To handle Universal Links (e.g., for QR login straight from the user's built-in camera app), you need to add the Associated Domains Entitlement to your App.entitlements file. To set up the entitlement in your app, open the target’s Signing & Capabilities tab in Xcode and add the Associated Domains capability, or if you already have entitlements you can modify your App.entitlements file to match this example:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:{domainName}</string>
</array>
</dict>
</plist>
This will handle all links with the following scheme: https://{yourCompany}.onekey.to?sessionId={sessionId}
Note: Keyri will create your https://{yourCompany}.onekey.to
page automatically once you configure it in the dashboard (opens in a new tab)
In the AppDelegate where the processing of links is declared, you need to add handlers in the application(_:continue:restorationHandler:)
method:
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL
else {
return false
}
let keyri = KeyriInterface(appKey: appKey)
keyri.processLink(url: incomingURL, payload: 'Custom', publicUserId: 'username')
return true
}
Note: Keyri will set up the required /.well-known/apple-app-site-association
JSON at your https://{yourSubdomain}.onekey.to
page as required by Apple to handle Universal Link handling. Details on this mechanism are described here: https://developer.apple.com/documentation/Xcode/supporting-associated-domains (opens in a new tab)
Android - App Links
To handle Android App Links (e.g., for QR login straight from the user's built-in camera app) you need to define the following intent-filter block in your AndroidManifest.xml:
<application...>
<!-- ... -->
<activity...>
<!-- ... -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="${domainName}" android:scheme="https" />
</intent-filter>
</activity>
</application>
In the activity where the processing of links is declared, you need to add handlers in the onNewIntent()
and onCreate()
methods, and pass `sessionId` to `easyKeyriAuth` method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
intent.data?.let(::process)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
process(intent.data)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Process result of easyKeyriAuth here
}
private fun processLink(data: Uri?) {
data?.getQueryParameters("sessionId")?.firstOrNull()?.let { sessionId ->
lifecycleScope.launch(Dispatchers.IO) {
easyKeyriAuth(
this@MainActivity,
123, // Request code (or you can use ActivityResult API)
"[Your appKey]", // Get this value from the Keyri dashboard
"[Your publicApiKey]", // Get this value from the Keyri dashboard, optional,
"[Your serviceEncryptionKey]", // Get this value from the Keyri dashboard, optional
true, // blockEmulatorDetection
"Custom payload here",
"public-User-ID", // publicUserId is optional
)
}
} ?: Log.e("Keyri", "Failed to process link")
}