cotter 0.1.0

  • Readme
  • Changelog
  • Example
  • Installing
  • 65

cotter #

A Flutter SDK for Cotter's Authentication Services. This package helps you add passwordless login to your app using the following methods:

  • [x] Sign in with device
  • [x] Sign in with email using OTP
  • [x] Sign in with phone number using OTP
  • [ ] Sign in with email using magic link
  • [ ] Sign in with phone number magic link

Getting Started #

As mentioned, there are 3 different ways to authenticate users. You can also combine the authentication methods, for example: Register the user after verifying their emails, then use Sign in with device for subsequent logins.

To use this SDK, you can create a free account at Cotter to get your API keys.

Sign in with device #

Signing in with device works like Google Prompt. It allows users to sign in to your website or app automatically from a device that they trust, or in one-tap by approving the login request from your app.

Signing Up #

To register a new user, we need to create a new user in Cotter and register the current device as trusted.

Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.signUpWithDevice(identifier: emailAddress);

  // You can also verify the user's email here, check 
  // "Sign in with Email > Verify Email for a logged-in user" below
  user = await user.verifyEmailWithOTP(redirectURL: "myexample://auth_callback");
} catch(e) {
  print(e);
}

Signing In #

To authenticate your user, the SDK will check and verify if the current device is trusted. If it is trusted, users can sign in automatically.

Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var event = await cotter.signInWithDevice(
    identifier: emailAddress,
    context: context,
  );
} catch(e) {
  print(e);
}
if (event.approved) {
  _goToDashboard();
}

Signing in from a Trusted Device

If the user signed-in from a trusted device, the event will automatically be approved and the user can proceed to the dashboard.

Signing in from a Non-Trusted Device

Otherwise, the SDK will show a prompt that asks the user to approve the login from the device that they trust. Inside your app in the trusted device, the SDK will show a prompt asking if the user want to approve the login.

// Show prompt to approve the login from the trusted device
Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.getUser();
  var event = await user.checkNewSignInRequest(context: context);
} catch(e) {
  print(e);
}

Sign in with Email / Phone Number #

You can authenticate users by sending a verification code to their email/phone and checking if they can enter the code correctly. Ideally, you would combine this method with the sign up with device method above. For example, there are several flows that you can use:

  1. Sign up and authenticate solely by sending a verification code to the user's email.
  2. Verify the user's email, then register user's device as trusted. Subsequent logins using sign in with device.
  3. Sign up with user's device, then verify user's email after registration. Subsequent logins using sign in with device.

Setup #

The email and phone number verification uses OAuth PKCE flow, which requires the app to open a secure in-app browser to authenticate and redirect back to the app with an authorization code.

To do this, you need to set up deep-linking.

Deep-linking in iOS #

Example URL: myexample://auth_callback. Add the following to your ios/Runner/Info.plist.

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLName</key>
    <string>myexample</string> <!-- 👈 Change this to your own URL Scheme -->
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myexample</string> <!-- 👈 Change this to your own URL Scheme -->
    </array>
  </dict>
</array>

Deep-linking in Android #

Example URL: myexample://auth_callback. Add the following to your android/app/src/main/AndroidManifest.xml.

<manifest ...>
    <application ...>

    <!-- Add the lines from here -->
    <activity android:name=".CallbackActivity" >
      <intent-filter android:label="flutter_web_auth">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- This is for myexample://auth_callback -->
        <!-- 👇 Change this to your own URL scheme -->
        <data android:scheme="myexample" android:host="auth_callback"/>
      </intent-filter>
    </activity>
    <!-- Until here -->

  </application>
</manifest>

Testing deep-linking #

Enter myexample://auth_callback in the simulator's browser and see if it redirects to your app.

Sign in with Email #

Make sure you have set up the deep-linking above.

Signing Up #

This method will:

  • Verify the user's email
  • Then create a new user in Cotter if successful
Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.signUpWithEmailOTP(
    redirectURL: "myexample://auth_callback",
    email: inputController.text, // Optional, if you leave this blank, user can enter email in the in-app browser
  );

  // If you want to follow flow 2 above, you can register the user's device as trusted here
  user = await user.registerDevice();
} catch(e) {
  print(e);
}

Signing in #

To authenticate by verifying user's email:

This method will create a new user if one doesn't exist.

Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.signInWithEmailOTP(
        redirectURL: "myexample://auth_callback",
        email: inputController.text, // Optional, if you leave this blank, user can enter email in the in-app browser
      );
} catch(e) {
  print(e);
}

Verify Email for a logged-in user #

To verify the email of a user that is currently logged-in:

Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.getUser();
  user = await user.verifyEmailWithOTP(redirectURL: "myexample://auth_callback");
} catch (e) {
  print(e);
}

Sign in with Phone #

Make sure you have set up the deep-linking above.

Signing Up #

This method will:

  • Verify the user's phone number
  • Then create a new user in Cotter if successful

Option 1: You want to use Cotter's input form inside the in-app browser. This helps with validating the input.

Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.signUpWithPhoneOTP(
    redirectURL: "myexample://auth_callback",
    channels: [PhoneChannel.SMS, PhoneChannel.WHATSAPP], // optional, default is SMS
  );
} catch (e) {
  print(e);
}

Option 2: You want to use your own input form and buttons. You can present 2 buttons to allow sending the OTP via WhatsApp or SMS.

  • Using SMS:
try {
  var user = await cotter.signUpWithPhoneOTPViaSMS(
              redirectURL: "myexample://auth_callback",
              phone: inputController.text,
            );
} catch (e) {
  print(e);
}
  • Using WhatsApp:
try {
  var user = await cotter.signUpWithPhoneOTPViaWhatsApp(
              redirectURL: "myexample://auth_callback",
              phone: inputController.text,
            );
} catch (e) {
  print(e);
}

Signing In #

To authenticate by verifying user's phone number:

Option 1: You want to use Cotter's input form inside the in-app browser. This helps with validating the input.

This method will create a new user if one doesn't exist.

Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.signInWithPhoneOTP(
    redirectURL: "myexample://auth_callback",
    channels: [PhoneChannel.SMS, PhoneChannel.WHATSAPP], // optional, default is SMS
  );
} catch (e) {
  print(e);
}

Option 2: You want to use your own input form and buttons. You can present 2 buttons to allow sending the OTP via WhatsApp or SMS.

  • Using SMS:
try {
  var user = await cotter.signInWithPhoneOTPViaSMS(
              redirectURL: "myexample://auth_callback",
              phone: inputController.text,
            );
} catch (e) {
  print(e);
}
  • Using WhatsApp:
try {
  var user = await cotter.signInWithPhoneOTPViaWhatsApp(
              redirectURL: "myexample://auth_callback",
              phone: inputController.text,
            );
} catch (e) {
  print(e);
}

Verify Phone for a logged-in user #

To verify the phone number of a user that is currently logged-in:

  • Using SMS:
Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.getUser();
  user = await user.verifyPhoneWithOTPViaSMS(redirectURL: "myexample://auth_callback");
} catch (e) {
  print(e);
}
  • Using WhatsApp:
Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.getUser();
  user = await user.verifyPhoneWithOTPViaWhatsApp(redirectURL: "myexample://auth_callback");
} catch (e) {
  print(e);
}

Getting the logged-in user #

The SDK automatically stores OAuth tokens (access token, id token, and refresh token) in the device's secure storage, along with the user information.

To get the logged-in user information:

Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var user = await cotter.getUser();
} catch(e) {
  print(e);
}

Getting OAuth Tokens #

The SDK can fetch stored tokens for you and refresh them as needed if they're expired.

Cotter cotter = new Cotter(apiKeyID: API_KEY_ID);
try {
  var accessToken = await cotter.getAccessToken();
  var idToken = await cotter.getIDToken();
  var refreshToken = await cotter.getRefreshToken();
} catch (e) {
  print(e);
}

0.0.1 #

  • Implement signing up and signing in with device.
  • Add functionality to store and retrieve signed-in user information.
  • Add functionality to store and retrieve oauth tokens and automatically refresh them.

0.0.2 #

  • Fix signInWithDevice error handling.

0.1.0 #

  • Implement sign in with email and phone number.

example/lib/main.dart

import 'package:example/dashboard.dart';
import 'package:example/register.dart';
import 'package:example/registerWithEmailVerification.dart';
import 'package:example/registerWithPhoneVerification.dart';
import 'package:flutter/material.dart';

var colors = {
  "light": Color(0xFFF3F3F3),
  "primary": Color(0xFF8650fa),
  "error": Color(0xFFF4416E),
};

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Cotter Example App',
      theme: ThemeData(
        primaryColor: colors["primary"],
      ),
      initialRoute: '/',
      routes: {
        // When navigating to the "/" route, build the FirstScreen widget.
        // '/': (context) => HomePage(),
        '/': (context) => HomePage(),
        // When navigating to the "/second" route, build the SecondScreen widget.
        Dashboard.routeName: (context) => Dashboard(),
        Register.routeName: (context) => Register(),
        RegisterWithEmailVerification.routeName: (context) =>
            RegisterWithEmailVerification(),
        RegisterWithPhoneVerification.routeName: (context) =>
            RegisterWithPhoneVerification(),
      },
    );
  }
}

class HomePage extends StatelessWidget {
  static String routeName = '/';

  @override
  Widget build(BuildContext context) {
    void _goToRegister() {
      Navigator.pushNamed(context, Register.routeName);
    }

    void _goToRegisterWithEmailVerification() {
      Navigator.pushNamed(context, RegisterWithEmailVerification.routeName);
    }

    void _goToRegisterWithPhoneVerification() {
      Navigator.pushNamed(context, RegisterWithPhoneVerification.routeName);
    }

    void _goToDashboard() {
      Navigator.pushNamed(context, Dashboard.routeName);
    }

    return Scaffold(
      body: Container(
          padding: EdgeInsets.symmetric(vertical: 20, horizontal: 16)
              .add(EdgeInsets.only(top: 80)),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              Expanded(
                child: Container(
                  child: Column(
                    children: [
                      Text(
                        "🐱",
                        style: TextStyle(fontSize: 40),
                      ),
                      Text(
                        "Welcome to Cotter Example App",
                        style: TextStyle(fontSize: 20),
                      ),
                    ],
                  ),
                ),
              ),
              Container(
                padding: EdgeInsets.symmetric(vertical: 30),
                child: Column(
                  children: [
                    ButtonTheme(
                      minWidth: double.infinity,
                      child: MaterialButton(
                        onPressed: () {
                          _goToRegister();
                        },
                        child: Text("Sign Up With Device"),
                        color: colors["primary"],
                        textColor: Colors.white,
                      ),
                    ),
                    ButtonTheme(
                      minWidth: double.infinity,
                      child: MaterialButton(
                        onPressed: () {
                          _goToRegisterWithEmailVerification();
                        },
                        child: Text("Sign Up With Email"),
                        color: colors["primary"],
                        textColor: Colors.white,
                      ),
                    ),
                    ButtonTheme(
                      minWidth: double.infinity,
                      child: MaterialButton(
                        onPressed: () {
                          _goToRegisterWithPhoneVerification();
                        },
                        child: Text("Sign Up With Phone"),
                        color: colors["primary"],
                        textColor: Colors.white,
                      ),
                    ),
                    ButtonTheme(
                      minWidth: double.infinity,
                      child: OutlineButton(
                        onPressed: () {
                          _goToDashboard();
                        },
                        child: Text("Go To Dashboard"),
                        color: colors["primary"],
                        textColor: colors["primary"],
                      ),
                    ),
                  ],
                ),
              ),
            ],
          )),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  cotter: ^0.1.0

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:cotter/cotter.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
38
Health:
Code health derived from static analysis. [more]
96
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
90
Overall:
Weighted score of the above. [more]
65
Learn more about scoring.

We analyzed this package on Jul 11, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.15
  • Flutter: 1.17.5

Analysis suggestions

Package does not support Flutter platform Linux

Because:

  • package:cotter/cotter.dart that imports:
  • package:cotter/src/tokens/cotterIDToken.dart that imports:
  • package:cotter/src/tokens/cotterJwtToken.dart that imports:
  • package:cotter/src/helper/jwt.dart that imports:
  • package:cotter/src/cotter.dart that imports:
  • package:cotter/src/models/user.dart that imports:
  • package:cotter/src/helper/storage.dart that imports:
  • package:flutter_secure_storage/flutter_secure_storage.dart that declares support for platforms: Android, iOS

Package does not support Flutter platform Web

Because:

  • package:cotter/cotter.dart that imports:
  • package:cotter/src/tokens/cotterIDToken.dart that imports:
  • package:cotter/src/tokens/cotterJwtToken.dart that imports:
  • package:cotter/src/helper/jwt.dart that imports:
  • package:cotter/src/cotter.dart that imports:
  • package:cotter/src/models/user.dart that imports:
  • package:cotter/src/helper/storage.dart that imports:
  • package:flutter_secure_storage/flutter_secure_storage.dart that declares support for platforms: Android, iOS

Package does not support Flutter platform Windows

Because:

  • package:cotter/cotter.dart that imports:
  • package:cotter/src/tokens/cotterIDToken.dart that imports:
  • package:cotter/src/tokens/cotterJwtToken.dart that imports:
  • package:cotter/src/helper/jwt.dart that imports:
  • package:cotter/src/cotter.dart that imports:
  • package:cotter/src/models/user.dart that imports:
  • package:cotter/src/helper/storage.dart that imports:
  • package:flutter_secure_storage/flutter_secure_storage.dart that declares support for platforms: Android, iOS

Package does not support Flutter platform macOS

Because:

  • package:cotter/cotter.dart that imports:
  • package:cotter/src/tokens/cotterIDToken.dart that imports:
  • package:cotter/src/tokens/cotterJwtToken.dart that imports:
  • package:cotter/src/helper/jwt.dart that imports:
  • package:cotter/src/cotter.dart that imports:
  • package:cotter/src/models/user.dart that imports:
  • package:cotter/src/helper/storage.dart that imports:
  • package:flutter_secure_storage/flutter_secure_storage.dart that declares support for platforms: Android, iOS

Package not compatible with SDK dart

Because:

  • cotter that is a package requiring null.

Health issues and suggestions

Document public APIs. (-0.85 points)

202 out of 203 API elements have no dartdoc comment.Providing good documentation for libraries, classes, functions, and other API elements improves code readability and helps developers find and use your API.

Fix lib/src/handlers/verify.dart. (-1.49 points)

Analysis of lib/src/handlers/verify.dart reported 3 hints:

line 1 col 8: Unused import: 'dart:convert'.

line 2 col 8: Unused import: 'dart:math'.

line 205 col 5: Avoid empty statements.

Fix lib/src/api.dart. (-0.50 points)

Analysis of lib/src/api.dart reported 1 hint:

line 99 col 32: This function has a return type of 'Future<Map<String, dynamic>>', but doesn't end with a return statement.

Fix lib/src/tokens/cotterJwtToken.dart. (-0.50 points)

Analysis of lib/src/tokens/cotterJwtToken.dart reported 1 hint:

line 1 col 8: Unused import: 'dart:convert'.

Fix lib/src/widgets/approveRequest.dart. (-0.50 points)

Analysis of lib/src/widgets/approveRequest.dart reported 1 hint:

line 5 col 7: This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: ApproveRequest.strings

Fix lib/src/widgets/authRequest.dart. (-0.50 points)

Analysis of lib/src/widgets/authRequest.dart reported 1 hint:

line 2 col 8: Unused import: 'package:cotter/src/models/approveRequestStrings.dart'.

Maintenance issues and suggestions

Support latest dependencies. (-10 points)

The version constraint in pubspec.yaml does not support the latest published versions for 1 dependency (jose).

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
async ^2.4.1 2.4.2
crypto ^2.1.4 2.1.5
cryptography ^1.2.1 1.4.1
device_info ^0.4.2 0.4.2+4
email_validator ^1.0.4 1.0.5
flutter 0.0.0
flutter_secure_storage ^3.3.3 3.3.3
flutter_web_auth ^0.2.4 0.2.4
http ^0.12.1 0.12.1
jose ^0.1.2+1 0.1.2+1 0.2.1+1
meta ^1.1.8 1.1.8 1.2.2
Transitive dependencies
built_collection 4.3.2
built_value 7.1.0
charcode 1.1.3
collection 1.14.12 1.14.13
convert 2.1.1
crypto_keys 0.1.3
fixnum 0.10.11
http_parser 3.1.4
js 0.6.2
matcher 0.12.8
path 1.7.0
pedantic 1.9.0 1.9.2
pointycastle 1.0.2
quiver 2.1.3
sky_engine 0.0.99
source_span 1.7.0
stack_trace 1.9.5
string_scanner 1.0.5
term_glyph 1.1.0
typed_data 1.1.6 1.2.0
vector_math 2.0.8 2.1.0-nullsafety
Dev dependencies
flutter_test