zero_tap_easy 0.1.1 copy "zero_tap_easy: ^0.1.1" to clipboard
zero_tap_easy: ^0.1.1 copied to clipboard

PlatformAndroid

Zero-tap sign-in restoration for Android. Silently recreate a signed-in session on a user's new device using Credential Manager Restore Credentials.

example/example.md

zero_tap_easy example #

Quick start #

import 'package:zero_tap_easy/zero_tap_easy.dart';

// After the user signs in, once per account per device:
if (await ZeroTapEasy.isSupported()) {
  final options = await api.getRegistrationOptions();
  final created = await ZeroTapEasy.createRestoreKey(options);
  await api.verifyRegistration(created.responseJson);
}

// On app launch, when nobody is signed in:
if (await ZeroTapEasy.isSupported()) {
  final options = await api.getAuthenticationOptions();
  final assertion = await ZeroTapEasy.getRestoreKey(options);
  if (assertion != null) {
    await api.verifyAssertion(assertion); // returns a session
  }
}

// On sign-out:
await ZeroTapEasy.clearRestoreKey();

Full integration in a sign-in flow #

import 'package:flutter/material.dart';
import 'package:zero_tap_easy/zero_tap_easy.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(home: const SignInPage());
}

class SignInPage extends StatefulWidget {
  const SignInPage({super.key});

  @override
  State<SignInPage> createState() => _SignInPageState();
}

class _SignInPageState extends State<SignInPage> {
  bool _checking = true;
  String _status = 'Checking for restore key…';

  @override
  void initState() {
    super.initState();
    _tryRestore();
  }

  Future<void> _tryRestore() async {
    try {
      if (!await ZeroTapEasy.isSupported()) {
        setState(() {
          _status = 'Restore keys not supported on this device.';
          _checking = false;
        });
        return;
      }

      // Request authentication options from your server.
      // Use an empty allowCredentials for discoverable credentials.
      final options = '{'
          '"challenge":"dGVzdA",'
          '"rpId":"example.com",'
          '"allowCredentials":[],'
          '"userVerification":"required"'
          '}';

      final assertion = await ZeroTapEasy.getRestoreKey(options);

      if (assertion != null) {
        // POST this to your server's verification endpoint.
        // The server identifies the user from the credential and returns
        // a session token. Then navigate to your home screen.
        setState(() {
          _status = 'Restored! Assertion ready for server verification.';
          _checking = false;
        });
      } else {
        setState(() {
          _status = 'No restore key. Show the sign-in screen.';
          _checking = false;
        });
      }
    } on ZeroTapException catch (e) {
      // A restore key is a convenience — never block launch.
      setState(() {
        _status = 'Restore failed (${e.code}). Show the sign-in screen.';
        _checking = false;
      });
    }
  }

  /// Call this after a successful sign-in to create a restore key
  /// for the user's next device.
  Future<void> _createRestoreKey() async {
    if (!await ZeroTapEasy.isSupported()) return;

    try {
      // Fetch registration options from your relying-party server.
      final options = '{'
          '"challenge":"xWiqF-ZQ8KRcFLeRBGnFSlGFiW1U_3r_iojyiqlqJ-w",'
          '"rp":{"id":"example.com","name":"My App"},'
          '"user":{"id":"dGVzdA","name":"user@example.com","displayName":"User"},'
          '"pubKeyCredParams":[{"type":"public-key","alg":-7}],'
          '"authenticatorSelection":{'
          '  "authenticatorAttachment":"platform",'
          '  "residentKey":"required",'
          '  "requireResidentKey":true,'
          '  "userVerification":"required"'
          '}'
          '}';

      final created = await ZeroTapEasy.createRestoreKey(options);

      // POST created.responseJson to your server's registration endpoint.
      debugPrint('Restore key created (cloud: ${created.usedCloudBackup})');
    } on ZeroTapException catch (e) {
      debugPrint('Could not create restore key: ${e.code}');
    }
  }

  /// Call this on sign-out.
  Future<void> _signOut() async {
    await ZeroTapEasy.clearRestoreKey();
    setState(() => _status = 'Signed out. Restore key cleared.');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('zero_tap_easy example')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (_checking) const CircularProgressIndicator(),
            Text(_status, style: Theme.of(context).textTheme.bodyLarge),
            const SizedBox(height: 24),
            FilledButton(
              onPressed: _checking ? null : _createRestoreKey,
              child: const Text('Create restore key'),
            ),
            const SizedBox(height: 12),
            OutlinedButton(
              onPressed: _checking ? null : _signOut,
              child: const Text('Sign out (clear key)'),
            ),
          ],
        ),
      ),
    );
  }
}

Unit testing with a fake #

import 'package:flutter_test/flutter_test.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:zero_tap_easy/zero_tap_easy.dart';

class FakeZeroTap extends ZeroTapEasyPlatform
    with MockPlatformInterfaceMixin {
  @override
  Future<bool> isSupported() async => true;

  @override
  Future<String> createRestoreKey(
    String requestJson, {
    required bool isCloudBackupEnabled,
  }) async => '{"id":"test","type":"public-key"}';

  @override
  Future<String?> getRestoreKey(String requestJson) async =>
      '{"sig":"test-assertion"}';

  @override
  Future<void> clearRestoreKey() async {}
}

void main() {
  setUp(() => ZeroTapEasyPlatform.instance = FakeZeroTap());

  test('getRestoreKey returns an assertion', () async {
    expect(await ZeroTapEasy.getRestoreKey('{}'), isNotNull);
  });
}

Running the full example app #

cd example
flutter run -d <device-id>

The full example app is in example/lib/main.dart — a test bench that exercises create, get, and clear with hand-entered WebAuthn JSON. See TESTING.md for device testing instructions.

6
likes
150
points
348
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Zero-tap sign-in restoration for Android. Silently recreate a signed-in session on a user's new device using Credential Manager Restore Credentials.

Repository (GitHub)
View/report issues

Topics

#authentication #android #credentials #passkeys #sign-in

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on zero_tap_easy

Packages that implement zero_tap_easy