Beacon Flutter Plugin

pub package GitHub

Connect Wallets with dApps on Tezos

Beacon is an implementation of the wallet interaction standard tzip-10 which describes the connection of a dApp with a wallet.

About

The Beacon Flutter Plugin provides Flutter developers with tools useful for setting up communication between native wallets supporting Tezos and dApps that implement beacon-sdk.

Platform Support

Android iOS MacOS Web Linux Windows
✔️ ✔️

Use this package as a library

Depend on it

Run this command:

With Flutter:

$ flutter pub add beacon_flutter

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
  beacon_flutter: latest

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

Import it Now in your Dart code, you can use:

import 'package:beacon_flutter/beacon_flutter.dart';

iOS Setup

iOS 14 and newer. Reason: Beacon iOS SDK

Android Setup

Create a new file proguard-rules.pro in app directory:

-keep class co.altme.alt.me.altme.** { *; } // Add your id
-keep class it.airgap.beaconsdk.** { *; }  
-keep class com.sun.jna.** { *; }
-keep class * implements com.sun.jna.** { *; }

# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
    static <1>$Companion Companion;
}

# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
    static **$* *;
}
-keepclassmembers class <2>$<3> {
    kotlinx.serialization.KSerializer serializer(...);
}

# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
    public static ** INSTANCE;
}
-keepclassmembers class <1> {
    public static <1> INSTANCE;
    kotlinx.serialization.KSerializer serializer(...);
}

# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

Modify your build.gradle in in app directory:

buildTypes {
    release {
        ...
        useProguard true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    debug {
        ...
    }
}

Screenshot

What -

What can you do with this package?

Starting beacon and listen to response

await _beaconPlugin.startBeacon();  
Future.delayed(const Duration(seconds: 1), (){
  _beaconPlugin.getBeaconResponse().listen(
    (data) {
        final Map<String, dynamic> requestJson =
            jsonDecode(data) as Map<String, dynamic>;
        final BeaconRequest beaconRequest =
            BeaconRequest.fromJson(requestJson);
            
        switch (beaconRequest.type) {
          case RequestType.permission:
            ...
            break;
          case RequestType.signPayload:
            ...
            break;
          case RequestType.operation:
            ...
            break;
          case RequestType.broadcast:
            ...
            break;
        }
    },
  );
}); 

Pairing wallet with dApp

try {  
  ...
  final Map response = await _beaconPlugin.pair(pairingRequest: pairingRequest);

  final bool success = json.decode(response['success'].toString()) as bool;

  if (success) {
      ...
  } else {
    throw ...;
  }
} catch (e) {
  ...
}

Pairing wallet with dApp using addPeer

try {  
  ...
  final Map response = await _beaconPlugin.addPeer(pairingRequest: pairingRequest);

  final bool success = json.decode(response['success'].toString()) as bool;

  if (success) {
      ...
  } else {
    throw ...;
  }
} catch (e) {
  ...
} 

convert pairingRequest to P2P

pairingRequestToP2P(pairingRequest: pairingRequest);  

Disconnecting with dApp

try {  
  ...
  final Map response = await _beaconPlugin.removePeerUsingPublicKey(publicKey: publicKey));

  final bool success = json.decode(response['success'].toString()) as bool;

  if (success) {
      ...
  } else {
    throw ...;
  }
} catch (e) {
  ...
}

Disconnecting with all dApps

try {  
  ...
  final Map response = await _beaconPlugin.removePeers());

  final bool success = json.decode(response['success'].toString()) as bool;

  if (success) {
      ...
  } else {
    throw ...;
  }
} catch (e) {
  ...
}

Sending permission response to dApp

try {  
  ...
  final Map response = await _beaconPlugin.permissionResponse(
    id: beaconRequest!.request!.id!,
    publicKey: publicKey, // publicKey of crypto account
    address: walletAddress, // walletAddress of crypto account
  );

  final bool success = json.decode(response['success'].toString()) as bool;

  if (success) {
      ...
  } else {
    throw ...;
  }
} catch (e) {
  ...
} 

Reject permission response to dApp

_beaconPlugin.permissionResponse(
  id: beaconRequest!.request!.id!,
  publicKey: null,
  address: null,
); 

Sending sign payload response to dApp

try {  
  ...
  //create signature using payload

  final Map response = await _beaconPlugin.signPayloadResponse(
    id: beaconRequest!.request!.id!,
    signature: signature,
  );

  final bool success = json.decode(response['success'].toString()) as bool;

  if (success) {
      ...
  } else {
    throw ...;
  }
} catch (e) {
  ...
} 

Reject sign payload response to dApp

_beaconPlugin.signPayloadResponse(
  id: beaconRequest!.request!.id!,
  signature: null,
);

Sending operation response to dApp

try {  
  ...
  // get transactionHash from the operation

  final Map response = await _beaconPlugin.operationResponse(
    id: beaconRequest!.request!.id!,
    transactionHash: transactionHash,
  );

  final bool success = json.decode(response['success'].toString()) as bool;

  if (success) {
      ...
  } else {
    throw ...;
  }
} catch (e) {
  ...
} 

Reject operation response to dApp

_beaconPlugin.operationResponse(
  id: beaconRequest!.request!.id!,
  transactionHash: null,
);

Sending broadcast response to dApp

try {  
  ...
  // get transactionHash using signedTransaction

  final Map response = await _beaconPlugin.broadcastResponse(
    id: beaconRequest!.request!.id!,
    transactionHash: transactionHash,
  );

  final bool success = json.decode(response['success'].toString()) as bool;

  if (success) {
      ...
  } else {
    throw ...;
  }
} catch (e) {
  ...
} 

Reject broadcast response to dApp

_beaconPlugin.broadcastResponse(
  id: beaconRequest!.request!.id!,
  transactionHash: null,
);

Get peers lists that is connected with dApp

final peers = await _beaconPlugin.getPeers();
final Map<String, dynamic> requestJson =
    jsonDecode(jsonEncode(peers)) as Map<String, dynamic>;
final ConnectedPeers connectedPeers =
    ConnectedPeers.fromJson(requestJson);

Example

import 'dart:convert';

import 'package:beacon_flutter/beacon_flutter.dart';
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Beacon Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Beacon Demo Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _beaconPlugin = Beacon();

  final TextEditingController pairingRequestController = TextEditingController(
      text:
          "GUsRsanpcLYPUk683gtFy4LJ6GAKP5BRe3jonDEQvXdCiYWnEGBq887akzYcKMbBnejMZMcFERAqzm8qqEHDnfPLyfNdjYVZ4qdGazMxu9X8iYeRSH7XUfCfoTfZMmnuQi5rccVEeM3JPRqZ1gUcyiuYQGBrEjyWH85JpV39GBcyw6Tkfiyauf2cUp4CYQqbbdiVRb5yLU3iogNXKn5wWKDBXj5HAHki7c12HgQvRqqiFJwsSPuv3Q8akazJkhX7adSuqEnvxo5LE15BdqM5GgXDic4ReSy3UTGNQbi3L2VXqb2yeiCfv5t1WAbQB1BB1NxT788yVRoS");

  bool hasPeers = false;

  String value = '';

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      startBeacon();
    });
  }

  startBeacon() async {
    final Map response = await _beaconPlugin.startBeacon();
    setState(() {
      hasPeers = json.decode(response['success'].toString());
    });
    getBeaconResponse();
  }

  void getBeaconResponse() {
    _beaconPlugin.getBeaconResponse().listen(
      (data) {
        setState(() {
          value = data.toString();
        });
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Beacon Demo'),
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            children: <Widget>[
              Container(
                alignment: Alignment.centerLeft,
                child: const Text('Pairing Request: '),
              ),
              TextField(
                controller: pairingRequestController,
                maxLines: 10,
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  ElevatedButton(
                    onPressed: !hasPeers
                        ? null
                        : () async {
                            final Map response =
                                await _beaconPlugin.removePeers();

                            setState(() {
                              bool success =
                                  json.decode(response['success'].toString());
                              hasPeers = !success;
                            });

                            if (!hasPeers) {
                              // ignore: use_build_context_synchronously
                              ScaffoldMessenger.of(context)
                                  .showSnackBar(const SnackBar(
                                content: Text('Successfully disconnected.'),
                              ));
                            }
                          },
                    child: const Text('Unpair'),
                  ),
                  ElevatedButton(
                    onPressed: hasPeers
                        ? null
                        : () async {
                            final Map response = await _beaconPlugin.pair(
                              pairingRequest: pairingRequestController.text,
                            );

                            setState(() {
                              bool success =
                                  json.decode(response['success'].toString());
                              hasPeers = success;
                            });

                            if (hasPeers) {
                              // ignore: use_build_context_synchronously
                              ScaffoldMessenger.of(context)
                                  .showSnackBar(const SnackBar(
                                content: Text('Successfully paired.'),
                              ));
                            } else {
                              // ignore: use_build_context_synchronously
                              ScaffoldMessenger.of(context)
                                  .showSnackBar(const SnackBar(
                                content: Text('Failed to pair.'),
                              ));
                            }
                          },
                    child: const Text('Pair'),
                  ),
                ],
              ),
              Container(
                alignment: Alignment.centerRight,
                child: ElevatedButton(
                  onPressed: value.isEmpty
                      ? null
                      : () async {
                          setState(() {
                            value = '';
                          });
                          await _beaconPlugin.respondExample();
                        },
                  child: const Text('Respond'),
                ),
              ),
              const Divider(),
              const SizedBox(height: 10),
              Container(
                alignment: Alignment.centerLeft,
                child: const Text('Beacon Response: '),
              ),
              SizedBox(
                width: double.infinity,
                child: Text(
                  value,
                  textAlign: TextAlign.left,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Author

Beacon Flutter Plugin is developed by Altme. Please feel free to contact us at thierry@altme.io.

Special Thanks

Tezos Foundation

Package Usage

Altme

Reference

License

Apache-2.0 (https://pub.dev/packages/beacon_flutter/license)