From 2168badf863d5ecef12e27d9a62e4d7f898005d2 Mon Sep 17 00:00:00 2001
From: vjrj <vjrj@comunes.org>
Date: Sun, 2 Jul 2023 12:35:50 +0200
Subject: [PATCH] Added NFC support (just testing)

---
 android/app/src/main/AndroidManifest.xml      |   1 +
 assets/translations/en.json                   |   3 +-
 assets/translations/es.json                   |   3 +-
 lib/ui/nfc_helper.dart                        |  56 ++++
 .../first_screen/pay_contact_search_page.dart | 249 ++++++++++--------
 .../second_screen/card_terminal_screen.dart   | 249 ++++++++++--------
 .../second_screen/card_terminal_status.dart   |  44 +++-
 pubspec.lock                                  |  16 ++
 pubspec.yaml                                  |   4 +-
 test/g1_test.dart                             |   2 +-
 10 files changed, 389 insertions(+), 238 deletions(-)
 create mode 100644 lib/ui/nfc_helper.dart

diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5e3c6a4d..c39b04d9 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.NFC" />
 
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
diff --git a/assets/translations/en.json b/assets/translations/en.json
index 0680d921..469230db 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -196,5 +196,6 @@
   "share_export_title": "Share your wallet",
   "share_export_desc": "Your wallet has been exported locally. Would you like to additionally share it with yourself via email/chat/etc. for safekeeping",
   "share_export_subject": "My Äž1nkgo Wallet",
-  "share_export_button": "SHARE"
+  "share_export_button": "SHARE",
+  "pay_with_nfc_tooltip": "To receive a payment, simply bring this device close to the other wallet with NFC activated."
 }
diff --git a/assets/translations/es.json b/assets/translations/es.json
index 9b6fc210..aa83be37 100644
--- a/assets/translations/es.json
+++ b/assets/translations/es.json
@@ -198,5 +198,6 @@
   "share_export_title": "Comporte tu monedero",
   "share_export_desc": "Tu monedero ha sido exportado localmente. ¿Te gustaría compartirlo adicionalmente contigo mismo vía email/chat/etc. para su resguardo?",
   "share_export_subject": "Mi monedero Äž1nkgo",
-  "share_export_button": "COMPARTIR"
+  "share_export_button": "COMPARTIR",
+  "pay_with_nfc_tooltip": "Para recibir un pago, simplemente acerca este dispositivo al otro monedero con NFC activado"
 }
diff --git a/lib/ui/nfc_helper.dart b/lib/ui/nfc_helper.dart
new file mode 100644
index 00000000..ad580807
--- /dev/null
+++ b/lib/ui/nfc_helper.dart
@@ -0,0 +1,56 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
+import 'package:ndef/ndef.dart' as ndef;
+import 'package:ndef/record.dart';
+import 'package:ndef/record/uri.dart';
+
+import 'logger.dart';
+
+bool hasNft(AsyncSnapshot<NFCAvailability> snapshot) =>
+    !kIsWeb && snapshot.hasData && snapshot.data == NFCAvailability.available;
+
+Future<void> writeNfcUrl(String url) async {
+  // timeout only works on Android, whereas the following two messages are only for iOS
+  final NFCTag tag = await FlutterNfcKit.poll(
+    timeout: const Duration(seconds: 10),
+    iosMultipleTagMessage: 'Multiple tags found!',
+    iosAlertMessage: 'Scan your tag',
+  );
+  final bool? ndefAvailable = tag.ndefAvailable;
+  final bool? ndefWritable = tag.ndefWritable;
+
+  if ((ndefAvailable == null || ndefWritable == null) &&
+          (ndefAvailable != null && !ndefAvailable) ||
+      (ndefWritable != null && !ndefWritable)) {
+    logger('Tag does not have NDEF capability or is not writable');
+    return;
+  }
+
+  try {
+    // Write a NDEF record with the URL to the tag
+    await FlutterNfcKit.writeNDEFRecords(
+        <NDEFRecord>[ndef.UriRecord.fromString(url)]);
+
+    // iOS only: show an alert message
+    await FlutterNfcKit.finish(iosAlertMessage: 'Success');
+  } catch (e) {
+    logger('Error while writing to tag: $e');
+    await FlutterNfcKit.finish(iosErrorMessage: 'Failed');
+  }
+}
+
+Future<String?> readNfcUrl() async {
+  final NFCTag tag =
+      await FlutterNfcKit.poll(timeout: const Duration(seconds: 10));
+  final bool? ndefAvailable = tag.ndefAvailable;
+  if (ndefAvailable != null && ndefAvailable) {
+    final List<NDEFRecord> records = await FlutterNfcKit.readNDEFRecords();
+    for (final NDEFRecord record in records) {
+      if (record is UriRecord) {
+        return record.uri.toString();
+      }
+    }
+  }
+  return null;
+}
diff --git a/lib/ui/widgets/first_screen/pay_contact_search_page.dart b/lib/ui/widgets/first_screen/pay_contact_search_page.dart
index 97e445f7..0f9f35e1 100644
--- a/lib/ui/widgets/first_screen/pay_contact_search_page.dart
+++ b/lib/ui/widgets/first_screen/pay_contact_search_page.dart
@@ -3,6 +3,7 @@ import 'dart:convert';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
 import 'package:http/http.dart';
 
 import '../../../data/models/contact.dart';
@@ -14,6 +15,7 @@ import '../../../g1/api.dart';
 import '../../../g1/g1_helper.dart';
 import '../../contacts_cache.dart';
 import '../../logger.dart';
+import '../../nfc_helper.dart';
 import '../../qr_manager.dart';
 import '../../ui_helpers.dart';
 import '../connectivity_widget_wrapper_wrapper.dart';
@@ -63,11 +65,11 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> {
       if (cPlusResponse.statusCode != 404) {
         // Add cplus users
         final List<dynamic> hits = ((const JsonDecoder()
-                .convert(cPlusResponse.body) as Map<String, dynamic>)['hits']
-            as Map<String, dynamic>)['hits'] as List<dynamic>;
+            .convert(cPlusResponse.body) as Map<String, dynamic>)['hits']
+        as Map<String, dynamic>)['hits'] as List<dynamic>;
         for (final dynamic hit in hits) {
           final Contact c =
-              await contactFromResultSearch(hit as Map<String, dynamic>);
+          await contactFromResultSearch(hit as Map<String, dynamic>);
           logger('Contact retrieved in c+ search $c');
           ContactsCache().addContact(c);
           setState(() {
@@ -87,11 +89,11 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> {
         // retrieve extra results with c+ profile
         for (final Contact wotC in wotResults) {
           final Contact cachedWotProfile =
-              await ContactsCache().getContact(wotC.pubKey);
+          await ContactsCache().getContact(wotC.pubKey);
           if (cachedWotProfile.name == null) {
             // Users without c+ profile
             final Contact cPlusProfile =
-                await getProfile(cachedWotProfile.pubKey, true);
+            await getProfile(cachedWotProfile.pubKey, true);
             ContactsCache().addContact(cPlusProfile);
           }
         }
@@ -122,107 +124,144 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> {
 
   @override
   Widget build(BuildContext context) {
-    final PaymentCubit paymentCubit = context.read<PaymentCubit>();
-    return Scaffold(
-      appBar: AppBar(
-        title: Text(tr('search_user_title')),
-        backgroundColor: Theme.of(context).colorScheme.primary,
-        foregroundColor: Theme.of(context).colorScheme.inversePrimary,
-        actions: <Widget>[
-          IconButton(
-              icon: const Icon(Icons.qr_code_scanner),
-              onPressed: () async {
-                final String? scannedKey = await QrManager.qrScan(context);
-                if (scannedKey is String &&
-                    scannedKey != null &&
-                    scannedKey != '-1') {
-                  final PaymentState? pay = parseScannedUri(scannedKey);
-                  if (pay != null) {
-                    logger('Scanned $pay');
-                    _searchTerm = extractPublicKey(pay.contact!.pubKey);
-                    await _search();
-                  }
-                  logger('QR result length ${_results.length}');
-                  if (_results.length == 1 && pay != null) {
-                    final Contact contact = _results[0];
-                    final double? currentAmount = paymentCubit.state.amount;
-                    paymentCubit.selectUser(contact);
-                    if (pay.amount != null) {
-                      paymentCubit.selectKeyAmount(contact, pay.amount);
-                    } else {
-                      paymentCubit.selectKeyAmount(contact, currentAmount);
-                    }
-                    if (pay.comment != null) {
-                      paymentCubit.setComment(pay.comment);
+    return FutureBuilder<NFCAvailability>(
+        future: FlutterNfcKit.nfcAvailability,
+        builder:
+            (BuildContext context, AsyncSnapshot<NFCAvailability> snapshot) {
+          final bool nft = hasNft(snapshot);
+
+          final PaymentCubit paymentCubit = context.read<PaymentCubit>();
+          return Scaffold(
+            appBar: AppBar(
+              title: Text(tr('search_user_title')),
+              backgroundColor: Theme
+                  .of(context)
+                  .colorScheme
+                  .primary,
+              foregroundColor: Theme
+                  .of(context)
+                  .colorScheme
+                  .inversePrimary,
+              actions: <Widget>[
+                if (nft) IconButton(
+                  icon: const Icon(Icons.nfc),
+                  onPressed: () async {
+                    final String? nfcUrl = await readNfcUrl();
+                    if (nfcUrl is String && nfcUrl != null && nfcUrl != '-1') {
+                      await _onKeyScanned(nfcUrl, paymentCubit);
                     }
-                  }
-                  if (!mounted) {
-                    return;
-                  }
-                  Navigator.pop(context);
-                }
-              }),
-          IconButton(
-            icon: const Icon(Icons.close),
-            onPressed: () => Navigator.pop(context),
-          )
-        ],
-      ),
-      body: Padding(
-        padding: const EdgeInsets.all(16.0),
-        child: Column(
-          crossAxisAlignment: CrossAxisAlignment.stretch,
-          children: <Widget>[
-            TextField(
-              controller: _searchController,
-              decoration: InputDecoration(
-                filled: true,
-                //fillColor: Colors.white,
-                labelText: tr('search_user'),
-                suffixIcon: IconButton(
-                  icon: const Icon(Icons.search),
-                  onPressed: () => _searchTerm.length < 3 ? null : _search(),
+                  },
                 ),
+                IconButton(
+                    icon: const Icon(Icons.qr_code_scanner),
+                    onPressed: () async {
+                      final String? scannedKey = await QrManager.qrScan(
+                          context);
+                      if (scannedKey is String &&
+                          scannedKey != null &&
+                          scannedKey != '-1') {
+                        await _onKeyScanned(scannedKey, paymentCubit);
+                        if (!mounted) {
+                          return;
+                        }
+                        Navigator.pop(context);
+                      }
+                    }),
+                IconButton(
+                  icon: const Icon(Icons.close),
+                  onPressed: () => Navigator.pop(context),
+                )
+              ],
+            ),
+            body: Padding(
+              padding: const EdgeInsets.all(16.0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.stretch,
+                children: <Widget>[
+                  TextField(
+                    controller: _searchController,
+                    decoration: InputDecoration(
+                      filled: true,
+                      //fillColor: Colors.white,
+                      labelText: tr('search_user'),
+                      suffixIcon: IconButton(
+                        icon: const Icon(Icons.search),
+                        onPressed: () =>
+                        _searchTerm.length < 3
+                            ? null
+                            : _search(),
+                      ),
+                    ),
+                    onChanged: (String value) {
+                      _searchTerm = value;
+                    },
+                    onSubmitted: (_) {
+                      _search();
+                    },
+                  ),
+                  if (_isLoading)
+                    const LoadingBox(simple: false)
+                  else
+                    if (_searchTerm.isNotEmpty && _results.isEmpty &&
+                        _isLoading)
+                      const NoElements(text: 'nothing_found')
+                    else
+                      Expanded(
+                        child: ListView.builder(
+                            itemCount: _results.length,
+                            itemBuilder: (BuildContext context, int index) {
+                              final Contact contact = _results[index];
+                              return FutureBuilder<Contact>(
+                                  future: ContactsCache().getContact(
+                                      contact.pubKey),
+                                  builder: (BuildContext context,
+                                      AsyncSnapshot<Contact> snapshot) {
+                                    Widget widget;
+                                    if (snapshot.hasData) {
+                                      widget =
+                                          _buildItem(
+                                              snapshot.data!, index, context);
+                                    } else if (snapshot.hasError) {
+                                      widget =
+                                          CustomErrorWidget(snapshot.error);
+                                    } else {
+                                      // Contact without wot
+                                      widget =
+                                          _buildItem(contact, index, context);
+                                    }
+                                    return widget;
+                                  });
+                            }),
+                      )
+                ],
               ),
-              onChanged: (String value) {
-                _searchTerm = value;
-              },
-              onSubmitted: (_) {
-                _search();
-              },
             ),
-            if (_isLoading)
-              const LoadingBox(simple: false)
-            else if (_searchTerm.isNotEmpty && _results.isEmpty && _isLoading)
-              const NoElements(text: 'nothing_found')
-            else
-              Expanded(
-                child: ListView.builder(
-                    itemCount: _results.length,
-                    itemBuilder: (BuildContext context, int index) {
-                      final Contact contact = _results[index];
-                      return FutureBuilder<Contact>(
-                          future: ContactsCache().getContact(contact.pubKey),
-                          builder: (BuildContext context,
-                              AsyncSnapshot<Contact> snapshot) {
-                            Widget widget;
-                            if (snapshot.hasData) {
-                              widget =
-                                  _buildItem(snapshot.data!, index, context);
-                            } else if (snapshot.hasError) {
-                              widget = CustomErrorWidget(snapshot.error);
-                            } else {
-                              // Contact without wot
-                              widget = _buildItem(contact, index, context);
-                            }
-                            return widget;
-                          });
-                    }),
-              )
-          ],
-        ),
-      ),
-    );
+          );
+        });
+  }
+
+  Future<void> _onKeyScanned(String scannedKey,
+      PaymentCubit paymentCubit) async {
+    final PaymentState? pay = parseScannedUri(scannedKey);
+    if (pay != null) {
+      logger('Scanned $pay');
+      _searchTerm = extractPublicKey(pay.contact!.pubKey);
+      await _search();
+    }
+    logger('QR result length ${_results.length}');
+    if (_results.length == 1 && pay != null) {
+      final Contact contact = _results[0];
+      final double? currentAmount = paymentCubit.state.amount;
+      paymentCubit.selectUser(contact);
+      if (pay.amount != null) {
+        paymentCubit.selectKeyAmount(contact, pay.amount);
+      } else {
+        paymentCubit.selectKeyAmount(contact, currentAmount);
+      }
+      if (pay.comment != null) {
+        paymentCubit.setComment(pay.comment);
+      }
+    }
   }
 
   Widget _buildItem(Contact contact, int index, BuildContext context) {
@@ -236,9 +275,9 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> {
       },
       trailing: BlocBuilder<ContactsCubit, ContactsState>(
           builder: (BuildContext context, ContactsState state) {
-        return ContactFavIcon(
-            contact: contact, contactsCubit: context.read<ContactsCubit>());
-      }),
+            return ContactFavIcon(
+                contact: contact, contactsCubit: context.read<ContactsCubit>());
+          }),
     );
   }
 }
diff --git a/lib/ui/widgets/second_screen/card_terminal_screen.dart b/lib/ui/widgets/second_screen/card_terminal_screen.dart
index 5036d1b9..3b9669b2 100644
--- a/lib/ui/widgets/second_screen/card_terminal_screen.dart
+++ b/lib/ui/widgets/second_screen/card_terminal_screen.dart
@@ -1,9 +1,11 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
 import 'package:qr_flutter/qr_flutter.dart';
 
 import '../../../g1/g1_helper.dart';
 import '../../../shared_prefs.dart';
+import '../../nfc_helper.dart';
 import '../../tutorial_keys.dart';
 import '../../ui_helpers.dart';
 import '../connectivity_widget_wrapper_wrapper.dart';
@@ -16,134 +18,149 @@ class CardTerminalScreen extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final String duniterUri = getQrUri(
-        pubKey: SharedPreferencesHelper().getPubKey(),
-        locale: context.locale.toLanguageTag(),
-        amount: amount);
-    return Card(
-      key: receiveQrKey,
-      elevation: 8,
-      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
-      child: Container(
-        width: double.infinity,
-        height: smallScreen(context) ? 200 : 252,
-        decoration: BoxDecoration(
-          borderRadius: BorderRadius.circular(8),
-          gradient: const LinearGradient(
-            begin: Alignment.topLeft,
-            end: Alignment.bottomRight,
-            colors: <Color>[
-              Colors.blueGrey,
-              Colors.white,
-            ],
-          ),
-        ),
-        child: Column(
-          crossAxisAlignment: CrossAxisAlignment.stretch,
-          children: <Widget>[
-            Container(
-                decoration: const BoxDecoration(
-                  borderRadius: BorderRadius.only(
-                    topLeft: Radius.circular(8),
-                    topRight: Radius.circular(8),
-                  ),
-                  gradient: LinearGradient(
-                    begin: Alignment.topLeft,
-                    end: Alignment.bottomRight,
-                    colors: <Color>[
-                      Color(0xFF3B3B3B),
-                      Color(0xFF232323),
-                    ],
-                  ),
-                ),
-                child: Row(
-                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                    children: <Widget>[
-                      ConnectivityWidgetWrapperWrapper(
-                          offlineWidget:
-                              const CardTerminalStatus(online: false),
-                          child: const CardTerminalStatus(online: true)),
-                      Padding(
-                        padding: const EdgeInsets.symmetric(horizontal: 10),
-                        child: Text(
-                          amount,
-                          textAlign: TextAlign.right,
-                          style: TextStyle(
-                            fontFamily: 'LCDMono',
-                            color: Colors.white,
-                            fontSize: amount.length < 5
-                                ? 28
-                                : amount.length < 10
-                                    ? 20
-                                    : amount.length < 15
-                                        ? 14
-                                        : 12,
-                            shadows: <Shadow>[
-                              Shadow(
-                                offset: const Offset(1, 1),
-                                blurRadius: 3,
-                                color: Colors.black.withOpacity(0.4),
-                              ),
-                            ],
-                            //softWrap: true, // Agrega esta línea para permitir que el texto se envuelva a la siguiente línea
-                          ),
-                        ),
-                      ),
-                    ])),
-            Expanded(
-                child: Column(children: <Widget>[
-              if (!amount.contains('+'))
-                Expanded(
-                    child: GestureDetector(
-                  onTap: () => copyPublicKeyToClipboard(context, duniterUri),
-                  child: QrImage(data: duniterUri),
-                  //   size: smallScreen(context) ? 95.0 : 140.0)
-                ))
-            ])),
-            Container(
-              decoration: const BoxDecoration(
-                borderRadius: BorderRadius.only(
-                  bottomLeft: Radius.circular(8),
-                  bottomRight: Radius.circular(8),
-                ),
-                gradient: LinearGradient(
+    return FutureBuilder<NFCAvailability>(
+        future: FlutterNfcKit.nfcAvailability,
+        builder:
+            (BuildContext context, AsyncSnapshot<NFCAvailability> snapshot) {
+          final String duniterUri = getQrUri(
+              pubKey: SharedPreferencesHelper().getPubKey(),
+              locale: context.locale.toLanguageTag(),
+              amount: amount);
+
+          final bool nft = hasNft(snapshot);
+          if (nft) {
+            writeNfcUrl(duniterUri);
+          }
+          return Card(
+            key: receiveQrKey,
+            elevation: 8,
+            shape:
+            RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
+            child: Container(
+              width: double.infinity,
+              height: smallScreen(context) ? 200 : 252,
+              decoration: BoxDecoration(
+                borderRadius: BorderRadius.circular(8),
+                gradient: const LinearGradient(
                   begin: Alignment.topLeft,
                   end: Alignment.bottomRight,
                   colors: <Color>[
-                    Color(0xFF232323),
-                    Color(0xFF3B3B3B),
+                    Colors.blueGrey,
+                    Colors.white,
                   ],
                 ),
               ),
-              child: Row(
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.stretch,
                 children: <Widget>[
-                  Expanded(
-                    child: Padding(
-                        padding: const EdgeInsets.symmetric(
-                            horizontal: 10, vertical: 6),
-                        child: Text.rich(
-                          TextSpan(
-                            children: <TextSpan>[
-                              TextSpan(
-                                text: amount.isNotEmpty
-                                    ? tr('show_qr_to_client_amount')
-                                    : tr('show_qr_to_client'),
+                  Container(
+                      decoration: const BoxDecoration(
+                        borderRadius: BorderRadius.only(
+                          topLeft: Radius.circular(8),
+                          topRight: Radius.circular(8),
+                        ),
+                        gradient: LinearGradient(
+                          begin: Alignment.topLeft,
+                          end: Alignment.bottomRight,
+                          colors: <Color>[
+                            Color(0xFF3B3B3B),
+                            Color(0xFF232323),
+                          ],
+                        ),
+                      ),
+                      child: Row(
+                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                          children: <Widget>[
+                            ConnectivityWidgetWrapperWrapper(
+                                offlineWidget:
+                                const CardTerminalStatus(online: false),
+                                child: const CardTerminalStatus(online: true)),
+                            Padding(
+                              padding:
+                              const EdgeInsets.symmetric(horizontal: 10),
+                              child: Text(
+                                amount,
+                                textAlign: TextAlign.right,
                                 style: TextStyle(
-                                  fontFamily: 'Roboto Mono',
-                                  color: Colors.grey,
-                                  fontSize: smallScreen(context) ? 12 : 14,
+                                  fontFamily: 'LCDMono',
+                                  color: Colors.white,
+                                  fontSize: amount.length < 5
+                                      ? 28
+                                      : amount.length < 10
+                                      ? 20
+                                      : amount.length < 15
+                                      ? 14
+                                      : 12,
+                                  shadows: <Shadow>[
+                                    Shadow(
+                                      offset: const Offset(1, 1),
+                                      blurRadius: 3,
+                                      color: Colors.black.withOpacity(0.4),
+                                    ),
+                                  ],
+                                  //softWrap: true, // Agrega esta línea para permitir que el texto se envuelva a la siguiente línea
                                 ),
                               ),
-                            ],
-                          ),
-                        )),
-                  )
+                            ),
+                          ])),
+                  Expanded(
+                      child: Column(children: <Widget>[
+                        if (!amount.contains('+'))
+                          Expanded(
+                              child: GestureDetector(
+                                onTap: () =>
+                                    copyPublicKeyToClipboard(
+                                        context, duniterUri),
+                                child: QrImage(data: duniterUri),
+                                //   size: smallScreen(context) ? 95.0 : 140.0)
+                              ))
+                      ])),
+                  Container(
+                    decoration: const BoxDecoration(
+                      borderRadius: BorderRadius.only(
+                        bottomLeft: Radius.circular(8),
+                        bottomRight: Radius.circular(8),
+                      ),
+                      gradient: LinearGradient(
+                        begin: Alignment.topLeft,
+                        end: Alignment.bottomRight,
+                        colors: <Color>[
+                          Color(0xFF232323),
+                          Color(0xFF3B3B3B),
+                        ],
+                      ),
+                    ),
+                    child: Row(
+                      children: <Widget>[
+                        Expanded(
+                          child: Padding(
+                              padding: const EdgeInsets.symmetric(
+                                  horizontal: 10, vertical: 6),
+                              child: Text.rich(
+                                TextSpan(
+                                  children: <TextSpan>[
+                                    TextSpan(
+                                      text: amount.isNotEmpty
+                                          ? tr('show_qr_to_client_amount')
+                                          : tr('show_qr_to_client'),
+                                      style: TextStyle(
+                                        fontFamily: 'Roboto Mono',
+                                        color: Colors.grey,
+                                        fontSize:
+                                        smallScreen(context) ? 12 : 14,
+                                      ),
+                                    ),
+                                  ],
+                                ),
+                              )),
+                        )
+                      ],
+                    ),
+                  ),
                 ],
               ),
             ),
-          ],
-        ),
-      ),
-    );
+          );
+        });
   }
 }
diff --git a/lib/ui/widgets/second_screen/card_terminal_status.dart b/lib/ui/widgets/second_screen/card_terminal_status.dart
index 29f44e91..154f5811 100644
--- a/lib/ui/widgets/second_screen/card_terminal_status.dart
+++ b/lib/ui/widgets/second_screen/card_terminal_status.dart
@@ -1,5 +1,9 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
+
+import '../../nfc_helper.dart';
+import '../../ui_helpers.dart';
 
 class CardTerminalStatus extends StatelessWidget {
   const CardTerminalStatus({super.key, required this.online});
@@ -8,18 +12,32 @@ class CardTerminalStatus extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Padding(
-      padding: const EdgeInsets.all(8.0),
-      child: Row(
-        children: <Widget>[
-          Tooltip(
-              message: online ? tr('online_terminal') : tr('offline_terminal'),
-              child: Icon(
-                Icons.payment,
-                color: online ? Colors.green : Colors.red,
-              )),
-        ],
-      ),
-    );
+    return FutureBuilder<NFCAvailability>(
+        future: FlutterNfcKit.nfcAvailability,
+        builder:
+            (BuildContext context, AsyncSnapshot<NFCAvailability> snapshot) {
+          final bool nft = hasNft(snapshot);
+          return Padding(
+            padding: const EdgeInsets.all(8.0),
+            child: Row(
+              children: <Widget>[
+                Tooltip(
+                    message:
+                        online ? tr('online_terminal') : tr('offline_terminal'),
+                    child: Icon(
+                      Icons.payment,
+                      color: online ? Colors.green : Colors.red,
+                    )),
+                if (nft || inDevelopment)
+                  Tooltip(
+                      message: tr(''),
+                      child: Icon(
+                        Icons.nfc,
+                        color: nft ? Colors.green : Colors.red,
+                      )),
+              ],
+            ),
+          );
+        });
   }
 }
diff --git a/pubspec.lock b/pubspec.lock
index 9a9adac0..941c148a 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -596,6 +596,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.2.0"
+  flutter_nfc_kit:
+    dependency: "direct main"
+    description:
+      name: flutter_nfc_kit
+      sha256: "47f0f5cda9343489ae729c3eedaae3b69d63564af8fe9c78496d6715f6a2e899"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.3.1"
   flutter_slidable:
     dependency: "direct main"
     description:
@@ -918,6 +926,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.4"
+  ndef:
+    dependency: "direct main"
+    description:
+      name: ndef
+      sha256: e40ece11d1cac52cba2b7d0211228c1b5c278032cce3f5bf3e2eefe3762fde6b
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.1"
   nested:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index a0694d43..5630d3ef 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
-version: 0.2.2
+version: 0.2.3-SNAPSHOT
 
 environment:
   sdk: ">=2.17.1 <3.0.0"
@@ -90,6 +90,8 @@ dependencies:
   toggle_switch: ^2.1.0
   fast_base58: ^0.2.1
   crypto: ^3.0.3
+  flutter_nfc_kit: ^3.3.1
+  ndef: ^0.3.1
 
 dev_dependencies:
   flutter_test:
diff --git a/test/g1_test.dart b/test/g1_test.dart
index c56b6b2f..ccedb7cd 100644
--- a/test/g1_test.dart
+++ b/test/g1_test.dart
@@ -58,7 +58,7 @@ void main() {
     const String baseKey = 'FRYyk57Pi456EJRu9vqVfSHLgmUfx4Qc3goS62a7dUSm';
     final String publicKeyWithChecksum = getFullPubKey(baseKey);
 
-    final List<String> keys = [baseKey, publicKeyWithChecksum];
+    final List<String> keys = <String>[baseKey, publicKeyWithChecksum];
 
     for (final String publicKey in keys) {
       final String uriA = getQrUri(pubKey: publicKey, amount: '10');
-- 
GitLab