mek_stripe_terminal 2.1.4 copy "mek_stripe_terminal: ^2.1.4" to clipboard
mek_stripe_terminal: ^2.1.4 copied to clipboard

A StripeTerminal plugin to discover readers, connect to them and process payments.

example/lib/main.dart

// ignore_for_file: avoid_print

import 'dart:async';
import 'dart:io';

import 'package:example/stripe_api.dart';
import 'package:flutter/material.dart';
import 'package:mek_stripe_terminal/mek_stripe_terminal.dart';
import 'package:permission_handler/permission_handler.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  print('Stripe Secret Key: ${StripeApi.secretKey.isNotEmpty}');

  runApp(const MaterialApp(
    home: MyApp(),
  ));
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _api = StripeApi();
  StripeTerminal? _terminal;

  var _locations = <Location>[];
  Location? _selectedLocation;

  StreamSubscription? _onConnectionStatusChangeSub;
  var _connectionStatus = ConnectionStatus.notConnected;
  bool _isSimulated = true;
  var _discoveringMethod = DiscoveryMethod.bluetoothScan;
  StreamSubscription? _discoverReaderSub;
  var _readers = const <Reader>[];
  StreamSubscription? _onUnexpectedReaderDisconnectSub;
  Reader? _reader;

  String? _paymentIntentClientSecret;
  PaymentIntent? _paymentIntent;
  CancelableFuture<PaymentIntent>? _collectingPaymentMethod;

  @override
  void dispose() {
    unawaited(_onConnectionStatusChangeSub?.cancel());
    unawaited(_discoverReaderSub?.cancel());
    unawaited(_onUnexpectedReaderDisconnectSub?.cancel());
    unawaited(_collectingPaymentMethod?.cancel());
    super.dispose();
  }

  Future<String> _fetchConnectionToken() async => _api.createTerminalConnectionToken();

  void _initTerminal() async {
    final permissions = [
      Permission.locationWhenInUse,
      Permission.bluetooth,
      if (Platform.isAndroid) ...[
        Permission.bluetoothScan,
        Permission.bluetoothConnect,
      ],
    ];

    for (final permission in permissions) {
      final result = await permission.request();
      print('$permission: $result');

      if (result == PermissionStatus.denied || result == PermissionStatus.permanentlyDenied) return;
    }

    final stripeTerminal = await StripeTerminal.getInstance(
      shouldPrintLogs: true,
      fetchToken: _fetchConnectionToken,
    );
    setState(() => _terminal = stripeTerminal);
    _onConnectionStatusChangeSub = stripeTerminal.onConnectionStatusChange.listen((event) {
      print('Connection Status Changed: ${event.name}');
      setState(() => _connectionStatus = event);
    });
    _onUnexpectedReaderDisconnectSub = stripeTerminal.onUnexpectedReaderDisconnect.listen((event) {
      print('Reader Unexpected Disconnected: ${event.label}');
      setState(() => _reader = null);
    });
  }

  void _fetchLocations(StripeTerminal terminal) async {
    setState(() => _locations = const []);
    final locations = await terminal.listLocations();
    setState(() => _locations = locations);
  }

  void _toggleLocation(Location location) {
    setState(() => _selectedLocation = _selectedLocation == location ? null : location);
  }

  void _changeMode() {
    setState(() {
      _isSimulated = !_isSimulated;
      _readers = const [];
    });
    _stopDiscoverReaders();
  }

  void _changeDiscoveryMethod(DiscoveryMethod? method) {
    setState(() {
      _discoveringMethod = method!;
      _readers = const [];
    });
  }

  void _checkStatus(StripeTerminal terminal) async {
    final status = await terminal.getConnectionStatus();
    _showSnackBar('Connection status: ${status.name}');
  }

  Future<Reader?> _connectReader(StripeTerminal terminal, Reader reader) async {
    String? getLocationId() {
      final locationId = _selectedLocation?.id ?? reader.locationId;
      if (locationId == null) _showSnackBar('Missing location');
      return locationId;
    }

    switch (_discoveringMethod) {
      case DiscoveryMethod.bluetoothScan || DiscoveryMethod.bluetoothProximity:
        final locationId = getLocationId();
        if (locationId == null) return null;
        return await terminal.connectBluetoothReader(
          reader,
          locationId: locationId,
        );
      case DiscoveryMethod.localMobile:
        final locationId = getLocationId();
        if (locationId == null) return null;
        return await terminal.connectMobileReader(
          reader,
          locationId: locationId,
        );
      case DiscoveryMethod.internet:
        return await terminal.connectInternetReader(reader);
      case DiscoveryMethod.handOff:
        return await terminal.connectHandoffReader(reader);
      case DiscoveryMethod.usb:
        final locationId = getLocationId();
        if (locationId == null) return null;
        return await terminal.connectUsbReader(reader, locationId: locationId);
      case DiscoveryMethod.embedded:
        _showSnackBar('Missing connect method implementation');
        return null;
    }
  }

  void _toggleReader(StripeTerminal terminal, Reader reader) async {
    if (_reader != null) {
      await terminal.disconnectReader();
      _showSnackBar('Terminal ${_reader!.label ?? _reader!.serialNumber} disconnected');
      setState(() => _reader = null);
      return;
    }

    final connectedReader = await _connectReader(terminal, reader);
    if (connectedReader == null) return;
    _showSnackBar(
        'Connected to a device: ${connectedReader.label ?? connectedReader.serialNumber}');
    setState(() => _reader = connectedReader);
  }

  void _startDiscoverReaders(StripeTerminal terminal) {
    setState(() => _readers = const []);

    final discoverReaderStream = terminal.discoverReaders(
      locationId: _selectedLocation?.id,
      discoveryMethod: _discoveringMethod,
      simulated: _isSimulated,
    );

    setState(() {
      _discoverReaderSub = discoverReaderStream.listen((readers) {
        setState(() => _readers = readers);
      }, onDone: () {
        setState(() => _discoverReaderSub = null);
      });
    });
  }

  void _stopDiscoverReaders() {
    unawaited(_discoverReaderSub?.cancel());
    setState(() => _discoverReaderSub = null);
  }

  void _createPaymentIntent() async {
    final paymentIntentClientSecret = await _api.createPaymentIntent();
    setState(() {
      _paymentIntentClientSecret = paymentIntentClientSecret;
      _paymentIntent = null;
    });
    _showSnackBar('Payment intent created!');
  }

  void _retrievePaymentIntent(StripeTerminal terminal, String paymentIntentClientSecret) async {
    final paymentIntent = await terminal.retrievePaymentIntent(paymentIntentClientSecret);
    setState(() => _paymentIntent = paymentIntent);
    _showSnackBar('Payment intent retrieved!');
  }

  void _collectPaymentMethod(StripeTerminal terminal, PaymentIntent paymentIntent) async {
    final collectingPaymentMethod = terminal.collectPaymentMethod(
      paymentIntent,
      skipTipping: true,
    );
    setState(() {
      _collectingPaymentMethod = collectingPaymentMethod;
    });

    try {
      final paymentIntentWithPaymentMethod = await collectingPaymentMethod;
      setState(() => _paymentIntent = paymentIntentWithPaymentMethod);
      _showSnackBar('Payment method collected!');
    } on TerminalException catch (exception) {
      switch (exception.rawCode) {
        case "2020" ||
              "cancelled": // TODO map error codes from swift/android and unify them for dart
          setState(() => _collectingPaymentMethod = null);
          _showSnackBar('Collecting Payment method is cancelled!');
        default:
          rethrow;
      }
    }
  }

  void _cancelCollectingPaymentMethod(CancelableFuture<PaymentIntent> cancelable) async {
    await cancelable.cancel();
  }

  void _processPayment(StripeTerminal terminal, PaymentIntent paymentIntent) async {
    final processedPaymentIntent = await terminal.processPayment(paymentIntent);
    setState(() => _paymentIntent = processedPaymentIntent);
    _showSnackBar('Payment processed!');
  }

  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      behavior: SnackBarBehavior.floating,
      content: Text(message),
    ));
  }

  @override
  Widget build(BuildContext context) {
    final terminal = _terminal;
    final paymentIntentClientSecret = _paymentIntentClientSecret;
    final paymentIntent = _paymentIntent;
    final collectingPaymentMethod = _collectingPaymentMethod;

    final mainTab = [
      TextButton(
        onPressed: terminal == null ? () async => _initTerminal() : null,
        child: const Text('Init Stripe'),
      ),
      TextButton(
        onPressed: () async => _api.createReader(),
        child: const Text('Random button'),
      ),
    ];
    final locationTab = [
      TextButton(
        onPressed: terminal != null ? () => _fetchLocations(terminal) : null,
        child: const Text('Fetch Locations'),
      ),
      const Divider(),
      ..._locations.map((e) {
        return ListTile(
          selected: _selectedLocation?.id == e.id,
          onTap: () => _toggleLocation(e),
          dense: true,
          title: Text('${e.id}: ${e.displayName}'),
          subtitle: Text('${e.address?.city},${e.address?.state},${e.address?.line1}'),
        );
      }),
    ];
    final readersTab = [
      TextButton(
        onPressed: terminal != null ? () => _checkStatus(terminal) : null,
        child: Text('Check status (${_connectionStatus.name})'),
      ),
      ListTile(
        onTap: _changeMode,
        title: const Text('Scanning mode'),
        trailing: Text(_isSimulated ? 'Simulator' : 'Real'),
      ),
      DropdownButton<DiscoveryMethod>(
        value: _discoveringMethod,
        onChanged: _changeDiscoveryMethod,
        items: DiscoveryMethod.values.map((e) {
          return DropdownMenuItem(
            value: e,
            child: Text(e.name),
          );
        }).toList(),
      ),
      if (_discoverReaderSub == null)
        TextButton(
          onPressed: terminal != null ? () => _startDiscoverReaders(terminal) : null,
          child: const Text('Scan Devices'),
        )
      else
        TextButton(
          onPressed: _stopDiscoverReaders,
          child: const Text('Stop Scanning'),
        ),
      const Divider(),
      ..._readers.map((e) {
        return ListTile(
          selected: e.serialNumber == _reader?.serialNumber,
          enabled: terminal != null &&
              _connectionStatus != ConnectionStatus.connecting &&
              (_reader == null || _reader!.serialNumber == e.serialNumber),
          onTap: terminal != null ? () => _toggleReader(terminal, e) : null,
          title: Text(e.serialNumber),
          subtitle: Text('${e.deviceType?.name ?? 'Unknown'} ${e.locationId ?? 'NoLocation'}'),
          trailing: Text('${(e.batteryLevel * 100).toInt()}'),
        );
      }),
    ];
    final paymentTab = [
      TextButton(
        onPressed: _createPaymentIntent,
        child: const Text('Create PaymentIntent'),
      ),
      TextButton(
        onPressed: terminal != null && paymentIntentClientSecret != null
            ? () => _retrievePaymentIntent(terminal, paymentIntentClientSecret)
            : null,
        child: const Text('Retrieve Payment Intent'),
      ),
      if (collectingPaymentMethod == null)
        TextButton(
          onPressed: terminal != null &&
                  _reader != null &&
                  paymentIntent != null &&
                  paymentIntent.status == PaymentIntentStatus.requiresPaymentMethod
              ? () => _collectPaymentMethod(terminal, paymentIntent)
              : null,
          child: const Text('Collect Payment Method'),
        )
      else
        TextButton(
          onPressed: () => _cancelCollectingPaymentMethod(collectingPaymentMethod),
          child: const Text('Cancel Collecting Payment Method'),
        ),
      TextButton(
        onPressed: terminal != null &&
                paymentIntent != null &&
                paymentIntent.status == PaymentIntentStatus.requiresConfirmation
            ? () => _processPayment(terminal, paymentIntent)
            : null,
        child: const Text('Process Payment'),
      ),
      const Divider(),
      if (paymentIntent != null)
        ListTile(
          title: Text('$paymentIntent'),
        )
    ];
    final cardTab = <Widget>[
      // TextButton(
      //   child: const Text('Read Reusable Card Detail'),
      //   onPressed: () async {
      //     stripeTerminal.readReusableCardDetail().then((StripePaymentMethod paymentMethod) {
      //       _showSnackbar(
      //         'A card was read: ${paymentMethod.cardDetails}',
      //       );
      //     });
      //   },
      // ),
      TextButton(
        onPressed: terminal != null
            ? () async => await terminal.setReaderDisplay(const Cart(
                  currency: 'USD',
                  tax: 130,
                  total: 1000,
                  lineItems: [
                    CartLineItem(
                      description: 'hello 1',
                      quantity: 1,
                      amount: 500,
                    ),
                    CartLineItem(
                      description: 'hello 2',
                      quantity: 1,
                      amount: 500,
                    ),
                  ],
                ))
            : null,
        child: const Text('Set reader display'),
      ),
    ];

    final tabs = {
      const Tab(text: 'Home'): mainTab,
      const Tab(text: 'Locations'): locationTab,
      const Tab(text: 'Readers'): readersTab,
      const Tab(text: 'Payment'): paymentTab,
      const Tab(text: 'Card'): cardTab,
    };

    return DefaultTabController(
      length: tabs.length,
      child: Scaffold(
        body: Column(
          children: [
            Material(
              color: Theme.of(context).colorScheme.primary,
              child: SafeArea(
                child: TabBar(
                  isScrollable: true,
                  tabs: tabs.keys.toList(),
                ),
              ),
            ),
            Expanded(
              child: TabBarView(
                children: tabs.values.map((e) {
                  return SingleChildScrollView(
                    child: Column(
                      children: e,
                    ),
                  );
                }).toList(),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
29
likes
150
points
1.73k
downloads

Publisher

unverified uploader

Weekly Downloads

A StripeTerminal plugin to discover readers, connect to them and process payments.

Repository (GitHub)
View/report issues

Topics

#stripe-terminal #mek-packages

Documentation

API reference

License

MIT (license)

Dependencies

collection, flutter, mek_data_class, meta, one_for_all, recase

More

Packages that depend on mek_stripe_terminal