partner_library_flutter 1.0.12 copy "partner_library_flutter: ^1.0.12" to clipboard
partner_library_flutter: ^1.0.12 copied to clipboard

Partner UI Integration Library for Flutter

example/lib/main.dart

import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:partner_library_flutter/partner_library_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await dotenv.load(fileName: "lib/.env");

  await PartnerLibrary.init(
    "https://sbmsmartbankinguat.esbeeyem.com",
    whitelistedDomains: [
      "razorpay.com",
      "sbmkycuat.esbeeyem.com",
      "m2pfintech.com"
    ],
    deviceBindingEnabled: false,
  );

  runApp(MyApp());
}


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,

      // ⬇️ Wrap everything in a “hostile shell” so back tries to pop parent first.
      builder: (context, child) => HostileShell(child: child ?? const SizedBox()),
      home: MyHome(),
    );
  }
}

/// Simulates a partner app that tries to pop routes on back.
/// Your WebView page should still consume back (keyboard → webview → pop).
class HostileShell extends StatelessWidget {
  final Widget child;
  const HostileShell({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false,
      onPopInvokedWithResult: (didPop, result) async {
        debugPrint('[HOST] back received at app-shell, attempting parent pop…');
        Navigator.of(context).maybePop();
      },
      child: child,
    );
  }
}


class MyHome extends StatefulWidget {
  @override
  _MyHomeState createState() => _MyHomeState();
}

class _MyHomeState extends State<MyHome> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _emailController = TextEditingController();
  final _phoneController = TextEditingController();

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

  Future<void> _restoreSavedFields() async {
    final prefs = await SharedPreferences.getInstance();
    _nameController.text = prefs.getString('name') ?? '';
    _emailController.text = prefs.getString('email') ?? '';
    _phoneController.text = prefs.getString('phone') ?? '';
  }

  @override
  void dispose() {
    _nameController.dispose();
    _emailController.dispose();
    _phoneController.dispose();
    super.dispose();
  }

  void _goToFlowRunner() {
    if (!_formKey.currentState!.validate()) return;

    final module = dotenv.env['MODULE']!;
    final baseUrl = dotenv.env['BASE_URL']!;
    final name = _nameController.text.trim();
    final email = _emailController.text.trim();
    final phone = _phoneController.text.trim();
    Navigator.of(context).push(MaterialPageRoute(
      builder: (_) => FlowRunnerPage(
        module: module,
        baseUrl: baseUrl,
        name: name,
        email: email,
        phone: phone,
        photo: '', // pass if you have one
      ),
    ));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Form(
            key: _formKey,
            child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
              Row(children: [
                const Spacer(),
                SizedBox(
                  height: 48,
                  child: TextButton(
                    onPressed: _goToFlowRunner,
                    child: const Text('Dashboard', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
                  ),
                ),
              ]),
              const SizedBox(height: 16),
              const Text('User Details', style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold)),
              const SizedBox(height: 16),
              TextFormField(
                controller: _nameController,
                decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()),
                validator: (v) => (v == null || v.isEmpty) ? 'Please enter your name' : null,
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: _emailController,
                decoration: const InputDecoration(labelText: 'Email', border: OutlineInputBorder()),
                validator: (v) => (v == null || v.isEmpty) ? 'Please enter your email' : null,
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: _phoneController,
                decoration: const InputDecoration(labelText: 'Phone number', border: OutlineInputBorder()),
                validator: (v) => (v == null || v.isEmpty) ? 'Please enter your phone number' : null,
              ),
            ]),
          ),
        ),
      ),
    );
  }
}

/// A single page that runs the whole “create token -> open PartnerLibrary” flow.
/// It shows a loader, handles errors, and keeps all async work off the form page.
class FlowRunnerPage extends StatefulWidget {
  final String module;
  final String baseUrl;
  final String name;
  final String email;
  final String phone;
  final String photo;

  const FlowRunnerPage({
    super.key,
    required this.module,
    required this.baseUrl,
    required this.name,
    required this.email,
    required this.phone,
    required this.photo,
  });

  @override
  State<FlowRunnerPage> createState() => _FlowRunnerPageState();
}

class _FlowRunnerPageState extends State<FlowRunnerPage> {
  String? _error;

  @override
  void initState() {
    super.initState();
    // kick off the flow after the first frame so `context` is fully available
    WidgetsBinding.instance.addPostFrameCallback((_) => _runFlow());
  }

  Future<void> _runFlow() async {
    try {
      // Save fields for next time (optional)
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('name', widget.name);
      await prefs.setString('email', widget.email);
      await prefs.setString('phone', widget.phone);

      // === previous createToken logic (dart_jsonwebtoken) ===
      final String kid = dotenv.env['KID']!;
      final String clientSecret = dotenv.env['CLIENT_SECRET']!;

      final Map<String, Object> headers = {
        'kid': kid,
        'typ': 'JWT',
        'alg': 'HS256',
      };

      final Map<String, String> attributes = {
        'name': widget.name,
        'photo': widget.photo,
      };

      final jwt = JWT(
        {
          'email': widget.email,
          'phone': widget.phone,
          'attributes': attributes,
          'module': widget.module,
        },
        header: headers,
      );

      // 5 minutes TTL
      final token = await jwt.sign(
        SecretKey(clientSecret),
        expiresIn: const Duration(minutes: 5),
      );

      if (!mounted) return;

      await PartnerLibrary.instance.open(
        context,
        widget.module,
        token,
            (WebViewCallback action) {
          switch (action.type) {
            case WebViewCallbackType.redirect:
              debugPrint("Redirected with status: ${action.status}");
              break;
            case WebViewCallbackType.logout:
              debugPrint("User logged out");
              break;
          }
        },
      );

      if (!mounted) return;
      Navigator.of(context).maybePop();
    } catch (e, st) {
      debugPrint('FlowRunner error: $e\n$st');
      if (!mounted) return;
      setState(() => _error = e.toString());
    }
  }


  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(title: const Text('Launching Dashboard')),
      body: Center(
        child: _error == null
            ? Column(
          mainAxisSize: MainAxisSize.min,
          children: const [
            CircularProgressIndicator(),
            SizedBox(height: 12),
            Text('Please wait… preparing session'),
          ],
        )
            : Padding(
          padding: const EdgeInsets.all(16),
          child: Column(mainAxisSize: MainAxisSize.min, children: [
            Icon(Icons.error_outline, size: 48, color: theme.colorScheme.error),
            const SizedBox(height: 12),
            Text('Something went wrong', style: theme.textTheme.titleMedium),
            const SizedBox(height: 8),
            Text(_error!, textAlign: TextAlign.center),
            const SizedBox(height: 16),
            FilledButton(
              onPressed: () {
                setState(() => _error = null);
                _runFlow();
              },
              child: const Text('Retry'),
            ),
          ]),
        ),
      ),
    );
  }
}