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:

  1. Automatically confirming (via confirmSession())

  2. Presenting your own custom confirmation screen to the user

  3. 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")
}