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 flutter_keyri

The SDK can then be imported into any dart file as follows:

import 'package:flutter_keyri/keyri.dart';

API

The Keyri Flutter API reference can be found on the pub.dev website: 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/keyri.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
 
void main() {
  runApp(const MyApp());
}
 
const String appKey = '[App Key Here]';
 
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();
 
  @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() async {
    await keyri
        .easyKeyriAuth(appKey, 'Some payload', 'Public user ID')
        .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(
        primary: Colors.deepPurple,
        onPrimary: Colors.white,
      ),
      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();
 
  void onMobileScannerDetect(Barcode barcode, MobileScannerArguments? args) {
    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
                ? Center(
                    child: Column(
                        mainAxisSize: MainAxisSize.max,
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: const [CircularProgressIndicator()]))
                : MobileScanner(allowDuplicates: false, onDetect: onMobileScannerDetect),
          )
        ],
      ),
    );
  }
 
  Future<void> _onReadSessionId(String sessionId) async {
    await keyri
        .initiateQrSession(appKey, sessionId, 'Public user ID')
        .then((session) => keyri
            .initializeDefaultScreen(sessionId, '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)));
 
    setState(() {
      _isLoading = false;
    });
  }
 
  void _onAuthResult(bool result) {
    String text;
 
    if (result) {
      text = 'Successfully authenticated!';
    } else {
      text = 'Failed to authenticate';
    }
 
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
 
    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 = Keyri()
    keyri.processLink(url: incomingURL)
 
    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)
}
 
private fun processLink(data: Uri?) {
    data?.getQueryParameters("sessionId")?.firstOrNull()?.let { sessionId ->
        lifecycleScope.launch(Dispatchers.IO) {
            val result = Keyri().easyKeyriAuth(
                supportFragmentManager,
                sessionId,
                "[Your appKey]", // Get this value from the Keyri dashboard
                "public-User-ID", // publicUserId is optional
                "Custom payload here"
            )
 
            // Process result
        }
    } ?: Log.e("Keyri", "Failed to process link")
}
Last updated on December 12, 2022