ab_ecosystem_sso 0.1.0
ab_ecosystem_sso: ^0.1.0 copied to clipboard
Flutter SDK for AB Ecosystem SSO — drop-in sign-in and sign-up widgets, 2FA, multi-account switching, captcha and secure session storage.
example/lib/main.dart
import 'package:ab_ecosystem_sso/ab_ecosystem_sso.dart';
import 'package:flutter/material.dart';
/// Point these at your own project. Both are required: the SDK is multi-tenant
/// and has no default for either.
const String kProjectId = 'YOUR_PROJECT_ID';
final Uri kBaseUrl = Uri.parse('https://api.ab-ecosystem.com');
final SsoClient sso = SsoClient(
SsoConfig(projectId: kProjectId, baseUrl: kBaseUrl),
);
Future<void> main() async {
// Reads the stored token and checks it with the server. Awaiting it before
// the first frame is what stops the app flashing the login screen for a
// user who is already signed in.
await sso.init();
runApp(SsoScope(client: sso, child: const ExampleApp()));
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SSO example',
theme: ThemeData(colorSchemeSeed: Colors.indigo),
darkTheme: ThemeData.dark(useMaterial3: true),
home: const Gate(),
);
}
}
/// Shows the auth flow or the app, depending on the session.
///
/// [SsoScope.sessionOf] rebuilds this whenever the session changes, so signing
/// in, signing out and a session going stale while backgrounded all land here.
class Gate extends StatelessWidget {
const Gate({super.key});
@override
Widget build(BuildContext context) {
final SsoSession session = SsoScope.sessionOf(context);
return switch (session) {
SsoUnknown() || SsoValidating() => const Scaffold(
body: Center(child: CircularProgressIndicator()),
),
SsoSignedIn() => const HomePage(),
// The login is still good — only this account went away. Offer the other
// accounts rather than signing the user out.
SsoAccountAccessLost(:final String? message) => Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (message != null)
Padding(
padding: const EdgeInsets.all(24),
child: Text(message, textAlign: TextAlign.center),
),
Expanded(
child: SsoAccountPicker.forSwitching(onSignedIn: (_) {}),
),
],
),
),
SsoSignedOut() => Scaffold(
body: SsoAuthFlow(
// The session notifier already drives this screen, so there is
// nothing to do here but let the rebuild happen.
onSignedIn: (String token) {},
),
),
};
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late Future<SsoProfile> _profile = SsoScope.of(context).profile();
@override
Widget build(BuildContext context) {
final SsoClient client = SsoScope.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Signed in'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.settings_outlined),
tooltip: 'Settings',
onPressed: () async {
// One screen for profile, password, 2FA, billing and — for an
// owner — roles, members and invitations.
await showSsoAccountSettings(
context,
inviteLinkBase: 'https://example.com/accept-invite',
onAccountSwitch: (_) =>
setState(() => _profile = client.profile()),
);
if (context.mounted) {
setState(() => _profile = client.profile());
}
},
),
IconButton(
icon: const Icon(Icons.logout),
tooltip: 'Sign out',
onPressed: client.logout,
),
],
),
body: FutureBuilder<SsoProfile>(
future: _profile,
builder: (BuildContext context, AsyncSnapshot<SsoProfile> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('${snapshot.error}'),
TextButton(
onPressed: () =>
setState(() => _profile = client.profile()),
child: const Text('Try again'),
),
],
),
);
}
final SsoProfile profile = snapshot.data!;
return ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
ListTile(
title: const Text('Name'),
subtitle: Text(profile.displayName),
),
ListTile(
title: const Text('Email'), subtitle: Text(profile.email)),
ListTile(
title: const Text('Account'),
subtitle: Text(client.context?.accountName ?? '—'),
),
ListTile(
title: const Text('Role'),
subtitle: Text(
client.context?.isOwner == true
? 'Owner'
: client.context?.role?.title ?? 'Member',
),
),
const Divider(),
ListTile(
title: const Text('Switch account'),
trailing: const Icon(Icons.chevron_right),
onTap: () => showModalBottomSheet<void>(
context: context,
builder: (BuildContext sheetContext) => Padding(
padding: const EdgeInsets.all(16),
child: SsoAccountPicker.forSwitching(
embedded: true,
onSignedIn: (_) {
Navigator.of(sheetContext).pop();
setState(() => _profile = client.profile());
},
),
),
),
),
],
);
},
),
);
}
}