sayari_oauth_flutter_sdk 0.1.0 copy "sayari_oauth_flutter_sdk: ^0.1.0" to clipboard
sayari_oauth_flutter_sdk: ^0.1.0 copied to clipboard

Reusable Sayari OAuth SDK for Flutter apps.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart' as http;

import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sayari_oauth_flutter_sdk/sayari_oauth_flutter_sdk.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const SayariSdkExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sayari OAuth Flutter SDK Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1800AD)),
        useMaterial3: true,
      ),
      home: const _ExampleHomePage(),
    );
  }
}

class _ExampleHomePage extends StatefulWidget {
  const _ExampleHomePage();

  @override
  State<_ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<_ExampleHomePage> {
  final _appLinks = AppLinks();
  final _loginHintController = TextEditingController();
  SayariOAuthFlutterSdk? _sdk;
  SayariOAuthTransaction? _transaction;
  SayariOAuthConfig? _config;
  String? _configError;
  String? _callbackText;
  String? _exchangeText;
  String? _lastAction;
  bool _loading = true;
  bool _launching = false;

  @override
  void initState() {
    super.initState();
    _loadConfig();
    _listenForCallbacks();
  }

  @override
  void dispose() {
    _loginHintController.dispose();
    super.dispose();
  }

  Future<void> _loadConfig() async {
    try {
      final raw = await rootBundle.loadString('assets/config/sayari-oauth-config.json');
      final decoded = jsonDecode(raw) as Map<String, dynamic>;
      final config = _parseConfig(decoded);
      final sdk = SayariOAuthFlutterSdk(config);
      final pkce = sdk.createPkce();

      setState(() {
        _config = config;
        _sdk = sdk;
        _transaction = sdk.createTransaction(pkce: pkce);
        _loading = false;
        _lastAction = 'Loaded config and created a PKCE transaction.';
      });
    } catch (error) {
      setState(() {
        _configError = error.toString();
        _loading = false;
      });
    }
  }

  void _listenForCallbacks() {
    _appLinks.uriLinkStream.listen((uri) {
      _handleIncomingUri(uri);
    });
    _appLinks.getInitialLink().then((uri) {
      if (uri != null) {
        _handleIncomingUri(uri);
      }
    });
  }

  void _handleIncomingUri(Uri uri) {
    final sdk = _sdk;
    final transaction = _transaction;
    if (sdk == null || transaction == null) {
      return;
    }

    final callback = sdk.parseCallback(uri);
    if (!transaction.validateCallback(callback)) {
      setState(() {
        _callbackText = 'Ignored callback: state mismatch or missing code.';
        _lastAction = 'Received a callback that did not match the transaction state.';
      });
      return;
    }

    setState(() {
      _callbackText = const JsonEncoder.withIndent('  ').convert({
        'code': callback.code,
        'state': callback.state,
        'error': callback.error,
        'errorDescription': callback.errorDescription,
        'redirectUri': callback.redirectUri,
      });
      _lastAction = 'Received a valid callback from Sayari.';
    });

    final code = callback.code;
    if (code != null) {
      unawaited(_exchangeCode(code, sdk, transaction, _config!));
    }
  }

  Future<void> _openAuthorize() async {
    final sdk = _sdk;
    final transaction = _transaction;
    final config = _config;
    if (sdk == null || transaction == null || config == null) {
      return;
    }

    setState(() {
      _launching = true;
      _lastAction = 'Launching the Sayari authorization flow.';
    });

    try {
      await sdk.launchAuthorize(
        pkce: transaction.pkce,
        redirectUri: config.redirectUri,
        appName: config.appName,
        appIcon: config.appIcon,
        parentOrigin: _deriveParentOrigin(config.redirectUri),
        loginHint: _loginHint(),
      );
    } catch (error) {
      setState(() {
        _lastAction = error.toString();
      });
    } finally {
      if (mounted) {
        setState(() {
          _launching = false;
        });
      }
    }
  }

  void _copyAuthorizeUri() {
    final sdk = _sdk;
    final transaction = _transaction;
    final config = _config;
    if (sdk == null || transaction == null || config == null) {
      return;
    }

    final uri = sdk.buildAuthorizeUri(
      pkce: transaction.pkce,
      redirectUri: config.redirectUri,
      appName: config.appName,
      appIcon: config.appIcon,
      parentOrigin: _deriveParentOrigin(config.redirectUri),
      loginHint: _loginHint(),
    );
    Clipboard.setData(ClipboardData(text: uri.toString()));
    setState(() {
      _lastAction = 'Authorize URL copied to clipboard.';
    });
  }

  SayariOAuthConfig _parseConfig(Map<String, dynamic> json) {
    final authApp = json['sayariAuthApp'] as Map<String, dynamic>;
    return SayariOAuthConfig(
      appId: authApp['appId'] as String,
      authorizeUrl: authApp['authorizeUrl'] as String,
      tokenUrl: authApp['tokenUrl'] as String,
      redirectUri: authApp['primaryRedirectUri'] as String,
      providerOrigin: authApp['providerOrigin'] as String,
      appName: json['appName'] as String,
      appIcon: authApp['appIcon'] as String?,
      backendBaseUrl: authApp['backendBaseUrl'] as String?,
    );
  }

  String? _loginHint() {
    final value = _loginHintController.text.trim();
    return value.isEmpty ? null : value;
  }

  Future<void> _exchangeCode(
    String code,
    SayariOAuthFlutterSdk sdk,
    SayariOAuthTransaction transaction,
    SayariOAuthConfig config,
  ) async {
    final backendUrl = config.backendExchangeUrl;
    if (backendUrl == null || backendUrl.isEmpty) {
      setState(() {
        _exchangeText = 'Backend exchange URL is not configured.';
      });
      return;
    }

    try {
      final response = await sdk.client.exchangeWithBackend(
        client: http.Client(),
        code: code,
        pkce: transaction.pkce,
        backendExchangeUrl: backendUrl,
        redirectUri: config.redirectUri,
      );

      setState(() {
        _exchangeText = _formatExchangeResponse(response);
        _lastAction = 'Sent authorization code to backend.';
      });
    } catch (error) {
      setState(() {
        _exchangeText = error.toString();
        _lastAction = 'Backend exchange failed.';
      });
    }
  }

  String _formatExchangeResponse(http.Response response) {
    final parsed = _tryParseJson(response.body);
    if (parsed is Map<String, dynamic>) {
      final session = parsed['session'];
      final sayari = session is Map<String, dynamic> ? session['sayari'] : null;

      return const JsonEncoder.withIndent('  ').convert({
        'ok': parsed['ok'],
        'message': parsed['message'],
        'statusCode': response.statusCode,
        'sessionUserName': session is Map<String, dynamic> ? session['userName'] : null,
        'sessionEmail': sayari is Map<String, dynamic> ? sayari['email'] : null,
        'sessionFullName': sayari is Map<String, dynamic> ? sayari['fullName'] : null,
        'sessionPhone': sayari is Map<String, dynamic> ? sayari['phoneNumber'] : null,
      });
    }

    return const JsonEncoder.withIndent('  ').convert({
      'statusCode': response.statusCode,
      'body': response.body,
    });
  }

  Object? _tryParseJson(String value) {
    try {
      return jsonDecode(value);
    } catch (_) {
      return null;
    }
  }
  String _deriveParentOrigin(String redirectUri) {
    final uri = Uri.parse(redirectUri);
    return Uri(scheme: uri.scheme, host: uri.host).toString();
  }

  @override
  Widget build(BuildContext context) {
    final config = _config;
    final transaction = _transaction;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Sayari OAuth Flutter SDK Example'),
      ),
      body: SafeArea(
        child: _loading
            ? const Center(child: CircularProgressIndicator())
            : ListView(
                padding: const EdgeInsets.all(20),
                children: [
                  if (_configError != null) _ErrorCard(message: _configError!),
                  if (config != null && transaction != null) ...[
                    _SectionCard(
                      title: 'Loaded config',
                      children: [
                        _Row(label: 'App name', value: config.appName!),
                        _Row(label: 'App ID', value: config.appId),
                        _Row(label: 'Authorize URL', value: config.authorizeUrl),
                        _Row(label: 'Redirect URI', value: config.redirectUri),
                        _Row(label: 'Provider origin', value: config.providerOrigin),
                      ],
                    ),
                    const SizedBox(height: 16),
                    _SectionCard(
                      title: 'PKCE transaction',
                      children: [
                        _Row(label: 'State', value: transaction.state),
                        _Row(label: 'Code verifier', value: transaction.codeVerifier),
                        _Row(label: 'Code challenge', value: transaction.codeChallenge),
                        _Row(label: 'Challenge method', value: transaction.pkce.challengeMethod),
                      ],
                    ),
                    const SizedBox(height: 16),
                    TextField(
                      controller: _loginHintController,
                      keyboardType: TextInputType.emailAddress,
                      autofillHints: const [AutofillHints.username, AutofillHints.email],
                      decoration: const InputDecoration(
                        labelText: 'Login hint (optional)',
                        helperText: 'Prefills the Sayari login identity when the browser opens the login page.',
                        border: OutlineInputBorder(),
                      ),
                    ),
                    const SizedBox(height: 16),
                    Wrap(
                      spacing: 12,
                      runSpacing: 12,
                      children: [
                        FilledButton.icon(
                          onPressed: _launching ? null : _openAuthorize,
                          icon: _launching
                              ? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2))
                              : const Icon(Icons.open_in_browser),
                          label: Text(_launching ? 'Opening...' : 'Open Sayari authorization'),
                        ),
                        OutlinedButton.icon(
                          onPressed: _copyAuthorizeUri,
                          icon: const Icon(Icons.copy),
                          label: const Text('Copy authorize URL'),
                        ),
                      ],
                    ),
                  ],
                  const SizedBox(height: 16),
                  _SectionCard(
                    title: 'Callback result',
                    children: [
                      Text(_callbackText ?? 'Waiting for a deep-link callback from Sayari.'),
                      const SizedBox(height: 12),
                      Text(_lastAction ?? 'Ready.'),
                    ],
                  ),
                  if (_exchangeText != null) ...[
                    const SizedBox(height: 16),
                    _SectionCard(
                      title: 'Backend exchange response',
                      children: [SelectableText(_exchangeText!)],
                    ),
                  ],
                  const SizedBox(height: 16),
                  const _SectionCard(
                    title: 'What this example shows',
                    children: [
                      Text('This app uses the reusable SDK package directly.'),
                      SizedBox(height: 8),
                      Text('It loads the example JSON asset, creates a PKCE transaction, opens the Sayari flow, and validates the deep-link callback.'),
                      SizedBox(height: 8),
                      Text('In a real app, the next step after callback is to send the authorization code to your own backend.'),
                    ],
                  ),
                ],
              ),
      ),
    );
  }
}

class _SectionCard extends StatelessWidget {
  const _SectionCard({required this.title, required this.children});

  final String title;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 0,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)),
            const SizedBox(height: 12),
            ...children,
          ],
        ),
      ),
    );
  }
}

class _Row extends StatelessWidget {
  const _Row({required this.label, required this.value});

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(label, style: Theme.of(context).textTheme.labelMedium),
          const SizedBox(height: 4),
          SelectableText(value),
        ],
      ),
    );
  }
}

class _ErrorCard extends StatelessWidget {
  const _ErrorCard({required this.message});

  final String message;

  @override
  Widget build(BuildContext context) {
    return Card(
      color: const Color(0xFFFFF1F0),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Text(message, style: const TextStyle(color: Color(0xFFB42318))),
      ),
    );
  }
}
2
likes
0
points
--
downloads

Publisher

unverified uploader

Weekly Downloads

Reusable Sayari OAuth SDK for Flutter apps.

Homepage

Topics

#oauth #authentication #sayari #flutter

License

unknown (license)

Dependencies

app_links, crypto, flutter, http, url_launcher

More

Packages that depend on sayari_oauth_flutter_sdk