Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- <uses-permission android:name="android.permission.NFC"/> -->
<uses-permission android:name="android.permission.NFC"/>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NFC was disabled probably because of stores publication process.

May be delayed into another release

cc @i5hi

<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />

Expand Down
5 changes: 5 additions & 0 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
07FFCC123142C862A5AA01A8 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
181C749B2E9401A4006ED365 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -152,6 +153,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
181C749B2E9401A4006ED365 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
Expand Down Expand Up @@ -488,6 +490,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 96;
DEVELOPMENT_TEAM = BX99T32YGS;
ENABLE_BITCODE = NO;
Expand Down Expand Up @@ -678,6 +681,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 96;
DEVELOPMENT_TEAM = BX99T32YGS;
ENABLE_BITCODE = NO;
Expand Down Expand Up @@ -706,6 +710,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 96;
DEVELOPMENT_TEAM = BX99T32YGS;
ENABLE_BITCODE = NO;
Expand Down
2 changes: 2 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>NFCReaderUsageDescription</key>
<string>Connect with your hardware wallet over NFC</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand Down
11 changes: 11 additions & 0 deletions ios/Runner/Runner.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>TAG</string>
<string>NDEF</string>
</array>
</dict>
</plist>
1 change: 1 addition & 0 deletions lib/core/entities/signer_device_entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ enum QrType { none, bbqr, urqr }

enum SignerDeviceEntity {
coldcardQ,
coldcardMk4,
jade,
keystone,
krux,
Expand Down
3 changes: 3 additions & 0 deletions lib/core/storage/tables/wallet_metadata_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ enum Signer {

enum SignerDevice {
coldcardQ,
coldcardMk4,
jade,
keystone,
krux,
Expand All @@ -38,6 +39,7 @@ enum SignerDevice {

static SignerDevice fromEntity(SignerDeviceEntity entity) => switch (entity) {
SignerDeviceEntity.coldcardQ => SignerDevice.coldcardQ,
SignerDeviceEntity.coldcardMk4 => SignerDevice.coldcardMk4,
SignerDeviceEntity.jade => SignerDevice.jade,
SignerDeviceEntity.keystone => SignerDevice.keystone,
SignerDeviceEntity.krux => SignerDevice.krux,
Expand All @@ -51,6 +53,7 @@ enum SignerDevice {

SignerDeviceEntity toEntity() => switch (this) {
SignerDevice.coldcardQ => SignerDeviceEntity.coldcardQ,
SignerDevice.coldcardMk4 => SignerDeviceEntity.coldcardMk4,
SignerDevice.jade => SignerDeviceEntity.jade,
SignerDevice.keystone => SignerDeviceEntity.keystone,
SignerDevice.krux => SignerDeviceEntity.krux,
Expand Down
179 changes: 179 additions & 0 deletions lib/core/widgets/nfc_bottom_sheet.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import 'dart:io' show Platform;

import 'package:bb_mobile/core/themes/app_theme.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/widgets/bottom_sheet/x.dart';
import 'package:bb_mobile/core/widgets/nfc_scanner_widget.dart';
import 'package:bb_mobile/core/widgets/snackbar_utils.dart';
import 'package:bb_mobile/core/widgets/text/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
import 'package:ndef/ndef.dart' as ndef;

class NfcBottomSheet {
static Future<void> showReadNfc({
required BuildContext context,
required String title,
required Future<void> Function(String payload) onDataReceived,
}) async {
await _showNfcBottomSheet(
context: context,
title: title,
onNfcAction: (tag) => _handleNfcRead(tag, context, onDataReceived),
);
}

static Future<void> showWriteNfc({
required BuildContext context,
required String title,
required String data,
required VoidCallback onSuccess,
}) async {
await _showNfcBottomSheet(
context: context,
title: title,
onNfcAction: (tag) => _writeNfcData(context, data, onSuccess),
);
}

static Future<void> _showNfcBottomSheet({
required BuildContext context,
required String title,
required Future<void> Function(NFCTag tag) onNfcAction,
}) async {
final isAvailable = await FlutterNfcKit.nfcAvailability;
if (isAvailable != NFCAvailability.available) {
if (context.mounted) {
final message = switch (isAvailable) {
NFCAvailability.disabled => 'NFC is disabled. Please enable NFC in your device settings',
NFCAvailability.not_supported => 'NFC is not supported on this device',
_ => 'NFC is not available on this device',
};
SnackBarUtils.showSnackBar(context, message);
}
return;
}

if (!context.mounted) {
return;
}

if (Platform.isIOS) {
try {
final tag = await FlutterNfcKit.poll(iosAlertMessage: title);
if (context.mounted) {
await onNfcAction(tag);
}
} catch (e) {
log.warning('NFC operation failed', error: e);
if (context.mounted &&
!e.toString().contains('Session invalidated by user')) {
SnackBarUtils.showSnackBar(context, 'NFC error: $e');
}
}
return;
}

await BlurredBottomSheet.show(
context: context,
isDismissible: true,
child: SafeArea(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: context.colour.onPrimary,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
height: 450,
child: Column(
children: [
BBText(
title,
style: context.font.headlineSmall,
textAlign: TextAlign.center,
),
Expanded(
child: NfcScannerWidget(
onScanned: (NFCTag tag) async {
if (context.mounted) {
Navigator.of(context).pop();
}
await onNfcAction(tag);
},
),
),
],
),
),
),
);
}

static Future<void> _handleNfcRead(
NFCTag tag,
BuildContext context,
Future<void> Function(String payload) onDataReceived,
) async {
try {
final ndefRecords = await FlutterNfcKit.readNDEFRecords();

if (ndefRecords.isEmpty) return;

final lastRecord = ndefRecords.last;

String payload = '';
if (lastRecord is ndef.ExternalRecord) {
payload =
lastRecord.payload
?.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join('') ??
'';
} else if (lastRecord is ndef.TextRecord) {
payload = lastRecord.text ?? '';
}

if (payload.isEmpty) return;

await onDataReceived(payload);
} catch (e) {
log.warning('Failed reading/parsing NDEF', error: e);
if (context.mounted &&
!e.toString().contains('Session invalidated by user')) {
SnackBarUtils.showSnackBar(context, 'NFC error: $e');
}
} finally {
try {
await FlutterNfcKit.finish();
} catch (_) {}
}
}

static Future<void> _writeNfcData(
BuildContext context,
String data,
VoidCallback onSuccess,
) async {
try {
await FlutterNfcKit.writeNDEFRecords([
ndef.TextRecord(
text: data,
language: 'en',
encoding: ndef.TextEncoding.UTF8,
),
]);

await FlutterNfcKit.finish();
onSuccess();
} catch (e) {
log.warning('Failed to send data via NFC: $e');
if (context.mounted &&
!e.toString().contains('Session invalidated by user')) {
SnackBarUtils.showSnackBar(context, 'NFC error: $e');
}
} finally {
try {
await FlutterNfcKit.finish();
} catch (_) {}
}
}
}
10 changes: 7 additions & 3 deletions lib/core/widgets/nfc_scanner_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,13 @@ class _NfcPageState extends State<NfcScannerWidget> {
final loadingWidget =
widget.loadingWidget ??
Center(
child: Gif(
image: AssetImage(Assets.animations.nfcPoll.path),
autostart: Autostart.loop,
child: SizedBox(
width: 250,
height: 250,
child: Gif(
image: AssetImage(Assets.animations.nfcPoll.path),
autostart: Autostart.loop,
),
),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:bb_mobile/core/themes/app_theme.dart';
import 'package:bb_mobile/core/widgets/buttons/button.dart';
import 'package:bb_mobile/core/widgets/inputs/paste_input.dart';
import 'package:bb_mobile/core/widgets/navbar/top_bar.dart';
import 'package:bb_mobile/core/widgets/nfc_bottom_sheet.dart';
import 'package:bb_mobile/core/widgets/text/text.dart';
import 'package:bb_mobile/core/widgets/transaction_details_widget.dart';
import 'package:bb_mobile/features/broadcast_signed_tx/presentation/broadcast_signed_tx_cubit.dart';
Expand Down Expand Up @@ -72,6 +73,21 @@ class BroadcastSignedTxPage extends StatelessWidget {
outlined: true,
),
const Gap(32),
BBButton.small(
label: 'NFC',
onPressed: () => NfcBottomSheet.showReadNfc(
context: context,
title: 'Tap your Coldcard to receive signed transaction via NFC',
onDataReceived: (payload) async {
await cubit.onQrScanned(payload);
},
),
bgColor: context.colour.onPrimary,
textColor: context.colour.secondary,
iconData: Icons.nfc,
outlined: true,
),
const Gap(32),
BBButton.small(
label: 'PushTx',
onPressed:
Expand All @@ -80,7 +96,7 @@ class BroadcastSignedTxPage extends StatelessWidget {
),
bgColor: context.colour.onPrimary,
textColor: context.colour.secondary,
iconData: Icons.nfc,
iconData: Icons.contactless_outlined,
outlined: true,
),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:bb_mobile/core/themes/app_theme.dart';
import 'package:bb_mobile/core/widgets/tab_menu_vertical_button.dart';
import 'package:bb_mobile/core/widgets/text/text.dart';
import 'package:bb_mobile/features/import_coldcard_q/router.dart';
import 'package:bb_mobile/features/import_coldcard/coldcard_device_selection_bottom_sheet.dart';
import 'package:bb_mobile/features/import_qr_device/router.dart';
import 'package:bb_mobile/features/ledger/ui/ledger_router.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -32,10 +32,8 @@ class ConnectHardwareWalletPage extends StatelessWidget {
child: Column(
children: [
TabMenuVerticalButton(
title: 'Coldcard Q',
onTap: () => context.pushNamed(
ImportColdcardQRoute.importColdcardQ.name,
),
title: 'Coldcard',
onTap: () => ColdcardDeviceSelectionBottomSheet.show(context),
),
const Gap(16),
TabMenuVerticalButton(
Expand Down
Loading