diff --git a/lib/screens/common/qrcode_bottom_sheet.dart b/lib/screens/common/qrcode_bottom_sheet.dart new file mode 100644 index 00000000..02dc1e1d --- /dev/null +++ b/lib/screens/common/qrcode_bottom_sheet.dart @@ -0,0 +1,52 @@ +import 'package:coconut_design_system/coconut_design_system.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:coconut_wallet/widgets/qrcode_info.dart'; + +// Usage: +// address_list_screen.dart +// wallet_info_screen.dart +class QrcodeBottomSheet extends StatelessWidget { + const QrcodeBottomSheet({super.key, required this.qrData, this.qrcodeTopWidget, this.title, this.isAddress = false}); + + final String qrData; + final Widget? qrcodeTopWidget; + final String? title; + final bool isAddress; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(CoconutStyles.radius_400), + child: Scaffold( + backgroundColor: CoconutColors.black, + appBar: AppBar( + title: Text(title ?? ''), + centerTitle: true, + backgroundColor: CoconutColors.black, + titleTextStyle: CoconutTypography.heading4_18, + toolbarTextStyle: CoconutTypography.body3_12, + leading: IconButton( + color: CoconutColors.white, + focusColor: CoconutColors.gray400, + icon: const Icon(CupertinoIcons.xmark, size: 20), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + automaticallyImplyLeading: false, + ), + body: SafeArea( + child: SingleChildScrollView( + child: Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height * 0.9, + color: CoconutColors.black, + child: QrCodeInfo(qrData: qrData, qrcodeTopWidget: qrcodeTopWidget, isAddress: isAddress), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/send/signed_psbt_scanner_screen.dart b/lib/screens/send/signed_psbt_scanner_screen.dart index b4c31908..136a8107 100644 --- a/lib/screens/send/signed_psbt_scanner_screen.dart +++ b/lib/screens/send/signed_psbt_scanner_screen.dart @@ -49,25 +49,27 @@ class _SignedPsbtScannerScreenState extends State { context: context, backgroundColor: CoconutColors.black.withOpacity(0.95), ), - body: Stack( - children: [ - // TODO: CoconutQrScanner -> AnimatedQrScanner로 Rename - CoconutQrScanner( - key: ValueKey(_qrScannerKey), - setMobileScannerController: _setQRViewController, - onComplete: _onCompletedScanning, - onFailed: _onFailedScanning, - qrDataHandler: _qrScanDataHandler, - ), - Padding( - padding: const EdgeInsets.only( - top: 20, - left: CoconutLayout.defaultPadding, - right: CoconutLayout.defaultPadding, + body: SafeArea( + child: Stack( + children: [ + // TODO: CoconutQrScanner -> AnimatedQrScanner로 Rename + CoconutQrScanner( + key: ValueKey(_qrScannerKey), + setMobileScannerController: _setQRViewController, + onComplete: _onCompletedScanning, + onFailed: _onFailedScanning, + qrDataHandler: _qrScanDataHandler, ), - child: _buildToolTip(), - ), - ], + Padding( + padding: const EdgeInsets.only( + top: 20, + left: CoconutLayout.defaultPadding, + right: CoconutLayout.defaultPadding, + ), + child: _buildToolTip(), + ), + ], + ), ), ), ); diff --git a/lib/screens/send/unsigned_transaction_qr_screen.dart b/lib/screens/send/unsigned_transaction_qr_screen.dart index 19784676..fa949d96 100644 --- a/lib/screens/send/unsigned_transaction_qr_screen.dart +++ b/lib/screens/send/unsigned_transaction_qr_screen.dart @@ -6,16 +6,14 @@ import 'package:coconut_wallet/enums/wallet_enums.dart'; import 'package:coconut_wallet/localization/strings.g.dart'; import 'package:coconut_wallet/providers/preference_provider.dart'; import 'package:coconut_wallet/providers/send_info_provider.dart'; -import 'package:coconut_wallet/styles.dart'; import 'package:coconut_wallet/utils/bb_qr/bb_qr_encoder.dart'; import 'package:coconut_wallet/utils/vibration_util.dart'; -import 'package:coconut_wallet/widgets/animated_qr/animated_qr_view.dart'; +import 'package:coconut_wallet/widgets/adaptive_qr_image.dart'; import 'package:coconut_wallet/widgets/animated_qr/view_data_handler/bc_ur_qr_view_handler.dart'; import 'package:coconut_wallet/widgets/button/fixed_bottom_button.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:qr_flutter/qr_flutter.dart'; import 'package:convert/convert.dart'; enum QrScanDensity { slow, normal, fast } @@ -120,125 +118,123 @@ class _UnsignedTransactionQrScreenState extends State { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: const Icon(Icons.close, color: CoconutColors.white), - onPressed: () { - Navigator.pop(context); - }, - ), - Text(t.faucet_request_bottom_sheet.title, style: CoconutTypography.body1_16), - Visibility( - visible: false, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - maintainSemantics: false, - maintainInteractivity: false, - child: IconButton( + return SafeArea( + child: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( icon: const Icon(Icons.close, color: CoconutColors.white), onPressed: () { Navigator.pop(context); }, ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 30), - Text(t.faucet_request_bottom_sheet.recipient, style: CoconutTypography.body1_16_Bold), - const SizedBox(height: 10), - CustomTextField( - controller: textController, - placeholder: t.faucet_request_bottom_sheet.placeholder, - onChanged: (text) { - _validateAddress(text.toLowerCase()); - }, - maxLines: 2, - style: CoconutTypography.body1_16_Number, - ), - const SizedBox(height: 2), - const SizedBox(height: 2), - Visibility( - visible: !_isErrorInAddress, - maintainSize: true, - maintainAnimation: true, - maintainState: true, - maintainSemantics: false, - maintainInteractivity: false, - child: Align( - alignment: Alignment.centerRight, - child: Text( - t.faucet_request_bottom_sheet.my_address(name: _walletName, index: _walletIndex), - style: CoconutTypography.body2_14_Number, + Text(t.faucet_request_bottom_sheet.title, style: CoconutTypography.body1_16), + Visibility( + visible: false, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + maintainSemantics: false, + maintainInteractivity: false, + child: IconButton( + icon: const Icon(Icons.close, color: CoconutColors.white), + onPressed: () { + Navigator.pop(context); + }, ), ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 24), - IgnorePointer( - ignoring: !canRequestFaucet(), - child: CupertinoButton( - onPressed: () { - widget.onRequest.call(_walletAddress, _requestAmount); - FocusScope.of(context).unfocus(); - }, - borderRadius: BorderRadius.circular(8.0), - padding: EdgeInsets.zero, - color: canRequestFaucet() ? CoconutColors.white : CoconutColors.white.withOpacity(0.3), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 12), - child: - _state == _AvailabilityState.checking - ? const SizedBox( - height: 28, - width: 28, - child: CircularProgressIndicator(color: CoconutColors.white), - ) - : Text( - _isRequesting - ? t.faucet_request_bottom_sheet.requesting - : t.faucet_request_bottom_sheet.request_amount( - bitcoin: _requestAmount.toTrimmedString(), - ), - style: CoconutTypography.body2_14 - .setColor( - (canRequestFaucet()) ? CoconutColors.black : CoconutColors.black.withOpacity(0.5), - ) - .merge(const TextStyle(letterSpacing: -0.1, fontWeight: FontWeight.w600)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 30), + MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.0)), + child: Text(t.faucet_request_bottom_sheet.recipient, style: CoconutTypography.body1_16_Bold), + ), + const SizedBox(height: 10), + MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.0)), + child: CustomTextField( + controller: textController, + placeholder: t.faucet_request_bottom_sheet.placeholder, + onChanged: (text) { + _validateAddress(text.toLowerCase()); + }, + maxLines: 2, + style: CoconutTypography.body1_16_Number, + ), + ), + const SizedBox(height: 2), + const SizedBox(height: 2), + MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.0)), + child: Visibility( + visible: !_isErrorInAddress, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + maintainSemantics: false, + maintainInteractivity: false, + child: Align( + alignment: Alignment.centerRight, + child: Text( + t.faucet_request_bottom_sheet.my_address(name: _walletName, index: _walletIndex), + style: CoconutTypography.body2_14_Number, ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + IgnorePointer( + ignoring: !canRequestFaucet(), + child: CupertinoButton( + onPressed: () { + widget.onRequest.call(_walletAddress, _requestAmount); + FocusScope.of(context).unfocus(); + }, + borderRadius: BorderRadius.circular(8.0), + padding: EdgeInsets.zero, + color: canRequestFaucet() ? CoconutColors.white : CoconutColors.white.withOpacity(0.3), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 12), + child: + _state == _AvailabilityState.checking + ? const SizedBox( + height: 28, + width: 28, + child: CircularProgressIndicator(color: CoconutColors.white), + ) + : Text( + _isRequesting + ? t.faucet_request_bottom_sheet.requesting + : t.faucet_request_bottom_sheet.request_amount( + bitcoin: _requestAmount.toTrimmedString(), + ), + style: CoconutTypography.body2_14 + .setColor( + (canRequestFaucet()) ? CoconutColors.black : CoconutColors.black.withOpacity(0.5), + ) + .merge(const TextStyle(letterSpacing: -0.1, fontWeight: FontWeight.w600)), + ), + ), ), ), - ), - const SizedBox(height: 4), - if (_state == _AvailabilityState.bad) ...{ - _buildWarningMessage(t.alert.faucet.no_test_bitcoin), - } else if (_state == _AvailabilityState.dailyLimitReached) ...{ - _buildWarningMessage(t.alert.faucet.try_again(count: _remainingTimeString)), - } else if (_isErrorInAddress) ...{ - _buildWarningMessage(t.alert.faucet.check_address), - }, - ], + const SizedBox(height: 4), + if (_state == _AvailabilityState.bad) ...{ + _buildWarningMessage(t.alert.faucet.no_test_bitcoin), + } else if (_state == _AvailabilityState.dailyLimitReached) ...{ + _buildWarningMessage(t.alert.faucet.try_again(count: _remainingTimeString)), + } else if (_isErrorInAddress) ...{ + _buildWarningMessage(t.alert.faucet.check_address), + }, + ], + ), ), ), ); diff --git a/lib/screens/wallet_detail/wallet_detail_screen.dart b/lib/screens/wallet_detail/wallet_detail_screen.dart index 24149acc..a984977d 100644 --- a/lib/screens/wallet_detail/wallet_detail_screen.dart +++ b/lib/screens/wallet_detail/wallet_detail_screen.dart @@ -66,39 +66,41 @@ class _WalletDetailScreenState extends State { Scaffold( backgroundColor: CoconutColors.black, appBar: _buildAppBar(context), - body: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - controller: _scrollController, - slivers: [ - CupertinoSliverRefreshControl(onRefresh: () async => _onRefresh()), - SliverToBoxAdapter( - child: Selector>( - selector: - (_, viewModel) => Tuple4( - AnimatedBalanceData(viewModel.balance, viewModel.prevBalance), - viewModel.bitcoinPriceKrwInString, - viewModel.sendingAmount, - viewModel.receivingAmount, - ), - builder: (_, data, __) { - return WalletDetailHeader( - key: _headerWidgetKey, - animatedBalanceData: data.item1, - currentUnit: _currentUnit, - btcPriceInKrw: data.item2, - sendingAmount: data.item3, - receivingAmount: data.item4, - onPressedUnitToggle: _toggleUnit, - onTapReceive: _onTapReceive, - onTapSend: _onTapSend, - ); - }, + body: SafeArea( + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + controller: _scrollController, + slivers: [ + CupertinoSliverRefreshControl(onRefresh: () async => _onRefresh()), + SliverToBoxAdapter( + child: Selector>( + selector: + (_, viewModel) => Tuple4( + AnimatedBalanceData(viewModel.balance, viewModel.prevBalance), + viewModel.bitcoinPriceKrwInString, + viewModel.sendingAmount, + viewModel.receivingAmount, + ), + builder: (_, data, __) { + return WalletDetailHeader( + key: _headerWidgetKey, + animatedBalanceData: data.item1, + currentUnit: _currentUnit, + btcPriceInKrw: data.item2, + sendingAmount: data.item3, + receivingAmount: data.item4, + onPressedUnitToggle: _toggleUnit, + onTapReceive: _onTapReceive, + onTapSend: _onTapSend, + ); + }, + ), ), - ), - _buildLoadingWidget(), - _buildTxListLabel(), - TransactionList(currentUnit: _currentUnit, walldtId: widget.id), - ], + _buildLoadingWidget(), + _buildTxListLabel(), + TransactionList(currentUnit: _currentUnit, walldtId: widget.id), + ], + ), ), ), _buildStickyHeader(), diff --git a/lib/widgets/adaptive_qr_image.dart b/lib/widgets/adaptive_qr_image.dart new file mode 100644 index 00000000..6dc97d24 --- /dev/null +++ b/lib/widgets/adaptive_qr_image.dart @@ -0,0 +1,51 @@ +import 'dart:math' as math; + +import 'package:coconut_design_system/coconut_design_system.dart'; +import 'package:coconut_wallet/screens/send/unsigned_transaction_qr_screen.dart'; +import 'package:coconut_wallet/widgets/animated_qr/animated_qr_view.dart'; +import 'package:coconut_wallet/widgets/animated_qr/view_data_handler/i_qr_view_data_handler.dart'; +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +class AdaptiveQrImage extends StatelessWidget { + const AdaptiveQrImage({super.key, this.qrData, this.qrDensity, this.qrViewDataHandler}); + + final String? qrData; + final QrScanDensity? qrDensity; + final IQrViewDataHandler? qrViewDataHandler; + + @override + Widget build(BuildContext context) { + assert(qrData != null || qrViewDataHandler != null, 'Either qrData or qrViewDataHandler must be provided'); + + return Container( + padding: const EdgeInsets.all(10), + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: CoconutBoxDecoration.shadowBoxDecoration, + child: LayoutBuilder( + builder: (context, constraints) { + final qrSize = _getQrSize(constraints); + if (qrData != null) { + return QrImageView(data: qrData!, size: qrSize); + } + + if (qrViewDataHandler != null) { + return AnimatedQrView( + key: ValueKey(qrDensity ?? QrScanDensity.normal), + qrViewDataHandler: qrViewDataHandler!, + qrScanDensity: qrDensity ?? QrScanDensity.normal, + qrSize: qrSize, + ); + } + + return const SizedBox.shrink(); + }, + ), + ); + } + + double _getQrSize(BoxConstraints constraints) { + final shortestScreenWidth = math.min(constraints.maxWidth, constraints.maxHeight); + return shortestScreenWidth.clamp(220, 360); + } +} diff --git a/lib/widgets/animated_qr/animated_qr_view.dart b/lib/widgets/animated_qr/animated_qr_view.dart index 8927c05d..2d911813 100644 --- a/lib/widgets/animated_qr/animated_qr_view.dart +++ b/lib/widgets/animated_qr/animated_qr_view.dart @@ -11,11 +11,13 @@ class AnimatedQrView extends StatefulWidget { final int milliSeconds; final QrScanDensity qrScanDensity; final IQrViewDataHandler qrViewDataHandler; + final double qrSize; const AnimatedQrView({ super.key, required this.qrViewDataHandler, required this.qrScanDensity, + required this.qrSize, this.milliSeconds = 600, }); @@ -69,7 +71,7 @@ class _AnimatedQrViewState extends State { // QR 전환이 바로 안될 때를 대비한 위젯 - 실제로는 렌더링 되지 않을 가능성 높음 return Stack( children: [ - QrImageView(data: _qrData, version: _qrVersion), + QrImageView(data: _qrData, version: _qrVersion, size: widget.qrSize), Positioned( left: 70, right: 70, @@ -93,7 +95,7 @@ class _AnimatedQrViewState extends State { // 시드사이너가 QR version 10 인 경우 빠르게 인식이 안되어 9로 설정합니다. // 아래 QrImageView의 maxInputLength는 2192bits(274bytes) - return QrImageView(data: _qrData, version: _qrVersion); + return QrImageView(data: _qrData, version: _qrVersion, size: widget.qrSize); } @override diff --git a/lib/widgets/animated_qr/coconut_qr_scanner.dart b/lib/widgets/animated_qr/coconut_qr_scanner.dart index 8695c9ee..b573eaf8 100644 --- a/lib/widgets/animated_qr/coconut_qr_scanner.dart +++ b/lib/widgets/animated_qr/coconut_qr_scanner.dart @@ -170,11 +170,7 @@ class _CoconutQrScannerState extends State with SingleTickerPr } Widget _buildProgressOverlay(BuildContext context) { - final scanAreaSize = - (MediaQuery.of(context).size.width < 400 || MediaQuery.of(context).size.height < 400) - ? 320.0 - : MediaQuery.of(context).size.width * 0.85; - + final scanAreaSize = ScannerOverlay.calculateScanAreaSize(context); final scanAreaTop = (MediaQuery.of(context).size.height - scanAreaSize) / 2; final scanAreaBottom = scanAreaTop + scanAreaSize; diff --git a/lib/widgets/animated_qr/view_data_handler/bc_ur_qr_view_handler.dart b/lib/widgets/animated_qr/view_data_handler/bc_ur_qr_view_handler.dart index ba3d865d..93543cd6 100644 --- a/lib/widgets/animated_qr/view_data_handler/bc_ur_qr_view_handler.dart +++ b/lib/widgets/animated_qr/view_data_handler/bc_ur_qr_view_handler.dart @@ -23,23 +23,21 @@ class BcUrQrViewHandler implements IQrViewDataHandler { cborEncoder.encodeBytes(input); final ur = UR(_urType, cborEncoder.getBytes()); // [Fast Mode] QR Code Ver 9 데이터 최대 크기(alphanumeric) 1840bits = 230bytes - // UR 헤더 길이: 약 20자 + // 하지만 실제 QR 라이브러리 제한은 더 작을 수 있음 (에러: 1628 > 1248 bytes) + // UR 헤더 길이: 약 20-30자 (시퀀스 정보 포함 시 더 길어질 수 있음) // 데이터: Bytewords.minimal로 인코딩(1바이트 -> 2자) - // 230 = 20 + (maxFragmentLen * 2) - // maxFragmentLen = (230 - 20) / 2 = 105 - // 하지만 105으로 설정 시 QrInputTooLongException: Input too long 에러가 발생하여 80으로 줄임 + // 안전한 값: (1248 - 30) / 2 = 609, 하지만 보수적으로 50으로 설정 + // 실제 테스트 결과 80에서도 에러 발생하므로 더 작게 조정 필요 // [Normal Mode] QR Code Ver 7 데이터 최대 크기(alphanumeric) 1232bits = 154bytes - // UR 헤더 길이: 약 20자 + // UR 헤더 길이: 약 20-30자 // 데이터: Bytewords.minimal로 인코딩(1바이트 -> 2자) - // 154 = 20 + (maxFragmentLen * 2) - // maxFragmentLen = (154 - 20) / 2 = 67 + // 안전한 값: (1248 - 30) / 2 = 609, 하지만 보수적으로 30으로 설정 // [Slow Mode] QR Code Ver 5 데이터 최대 크기(alphanumeric) 848bits = 106bytes - // UR 헤더 길이: 약 20자 + // UR 헤더 길이: 약 20-30자 // 데이터: Bytewords.minimal로 인코딩(1바이트 -> 2자) - // 106 = 20 + (maxFragmentLen * 2) - // maxFragmentLen = (106 - 20) / 2 = 43 + // 안전한 값: (106 - 30) / 2 = 38, 하지만 보수적으로 15로 설정 // ver 최소 셀 수 | 데이터 최대 크기 (errorCorrectionLevel: Low 기준, bytes) // 1: 21 * 21 | 17 @@ -57,12 +55,14 @@ class BcUrQrViewHandler implements IQrViewDataHandler { printLongString('--> source: ${UREncoder.encode(ur)}'); + // QrInputTooLongException 방지를 위해 maxFragmentLen을 더 작게 설정 + // 실제 QR 라이브러리 제한(1248 bytes)을 고려하여 보수적으로 설정 int maxFragmentLen = qrScanDensity == QrScanDensity.fast - ? 80 + ? 50 // 80에서 50으로 감소 (안전 마진 확보) : qrScanDensity == QrScanDensity.normal - ? 40 - : 20; + ? 30 // 40에서 30으로 감소 + : 15; // 20에서 15로 감소 _urEncoder = UREncoder(ur, maxFragmentLen); } diff --git a/lib/widgets/body/address_qr_scanner_body.dart b/lib/widgets/body/address_qr_scanner_body.dart index ee8d25f4..32c144aa 100644 --- a/lib/widgets/body/address_qr_scanner_body.dart +++ b/lib/widgets/body/address_qr_scanner_body.dart @@ -40,42 +40,58 @@ class _AddressQrScannerBodyState extends State { Widget _buildQrView(BuildContext context) { // 스캔 영역을 상단으로 이동 (상단 여백 120px 추가) final statusBarHeight = MediaQuery.of(context).padding.top; - final topMargin = statusBarHeight + 120.0; + final isFoldScreen = MediaQuery.of(context).size.width > 600; + final topMargin = statusBarHeight + (isFoldScreen ? 0 : 120.0); + return LayoutBuilder( + builder: (context, constraints) { + final Size layoutSize = constraints.biggest; - return Stack( - children: [ - MobileScanner( - key: widget.qrKey, - controller: _controller, - onDetect: widget.onDetect, - errorBuilder: (context, error) { - if (error.errorCode == MobileScannerErrorCode.permissionDenied && !_isShowedCameraPermissionDialog) { - _isShowedCameraPermissionDialog = true; - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!mounted) return; - await _showCameraPermissionDialog(context); - if (!mounted) return; - Navigator.pop(context); - }); - } - return Center(child: Text(error.errorCode.message)); - }, - ), - const ScannerOverlay(), - Positioned( - top: topMargin, - left: 0, - right: 0, - child: Container( - padding: const EdgeInsets.only(top: 32), - child: Text( - t.send_address_screen.text2, - textAlign: TextAlign.center, - style: CoconutTypography.body1_16.setColor(CoconutColors.white), + // ScannerOverlay와 동일한 크기의 정사각형 스캔 영역 계산 + final scanAreaSize = ScannerOverlay.calculateScanAreaSize(context); + + final Rect scanWindow = Rect.fromCenter( + center: layoutSize.center(Offset.zero), + width: scanAreaSize, + height: scanAreaSize, + ); + + return Stack( + children: [ + MobileScanner( + key: widget.qrKey, + controller: _controller, + onDetect: widget.onDetect, + scanWindow: scanWindow, + errorBuilder: (context, error) { + if (error.errorCode == MobileScannerErrorCode.permissionDenied && !_isShowedCameraPermissionDialog) { + _isShowedCameraPermissionDialog = true; + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + await _showCameraPermissionDialog(context); + if (!mounted) return; + Navigator.pop(context); + }); + } + return Center(child: Text(error.errorCode.message)); + }, ), - ), - ), - ], + const ScannerOverlay(), + Positioned( + top: topMargin, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.only(top: 32), + child: Text( + t.send_address_screen.text2, + textAlign: TextAlign.center, + style: CoconutTypography.body1_16.setColor(CoconutColors.white), + ), + ), + ), + ], + ); + }, ); } diff --git a/lib/widgets/overlays/scanner_overlay.dart b/lib/widgets/overlays/scanner_overlay.dart index 328c092d..197eec13 100644 --- a/lib/widgets/overlays/scanner_overlay.dart +++ b/lib/widgets/overlays/scanner_overlay.dart @@ -6,13 +6,21 @@ class ScannerOverlay extends StatelessWidget { @override Widget build(BuildContext context) { - final scanAreaSize = - (MediaQuery.of(context).size.width < 400 || MediaQuery.of(context).size.height < 400) - ? 320.0 - : MediaQuery.of(context).size.width * 0.85; + final scanAreaSize = calculateScanAreaSize(context); return CustomPaint(size: MediaQuery.of(context).size, painter: _ScannerOverlayPainter(scanAreaSize)); } + + static double calculateScanAreaSize(BuildContext context) { + final size = MediaQuery.of(context).size; + final isWideScreen = size.width > 600; + + return (size.width < 400 || size.height < 400) + ? 320.0 + : isWideScreen + ? 500.0 + : size.width * 0.85; + } } class _ScannerOverlayPainter extends CustomPainter { diff --git a/lib/widgets/qrcode_info.dart b/lib/widgets/qrcode_info.dart index d68e11b0..9d3deb4a 100644 --- a/lib/widgets/qrcode_info.dart +++ b/lib/widgets/qrcode_info.dart @@ -1,7 +1,7 @@ import 'package:coconut_design_system/coconut_design_system.dart'; +import 'package:coconut_wallet/widgets/adaptive_qr_image.dart'; import 'package:coconut_wallet/widgets/button/copy_text_container.dart'; import 'package:flutter/material.dart'; -import 'package:qr_flutter/qr_flutter.dart'; class QrCodeInfo extends StatefulWidget { final String qrData; @@ -17,31 +17,21 @@ class QrCodeInfo extends StatefulWidget { class _QrCodeInfoState extends State { @override Widget build(BuildContext context) { - final double qrSize = MediaQuery.of(context).size.width * 275 / 375; - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 36), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (widget.qrcodeTopWidget != null) ...[widget.qrcodeTopWidget!, CoconutLayout.spacing_400h], - Stack( - children: [ - Container( - width: qrSize, - height: qrSize, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(CoconutStyles.radius_200), - color: CoconutColors.white, - ), - ), - QrImageView(data: widget.qrData, version: QrVersions.auto, size: qrSize), - ], + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (widget.qrcodeTopWidget != null) ...[widget.qrcodeTopWidget!, CoconutLayout.spacing_400h], + AdaptiveQrImage(qrData: widget.qrData), + const SizedBox(height: 32), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: CopyTextContainer( + text: widget.qrData, + textStyle: CoconutTypography.body2_14, + isAddress: widget.isAddress, ), - CoconutLayout.spacing_600h, - CopyTextContainer(text: widget.qrData, textStyle: CoconutTypography.body2_14, isAddress: widget.isAddress), - ], - ), + ), + ], ); } } diff --git a/lib/widgets/tooltip/faucet_tooltip.dart b/lib/widgets/tooltip/faucet_tooltip.dart index e5165f65..c8d1d0fa 100644 --- a/lib/widgets/tooltip/faucet_tooltip.dart +++ b/lib/widgets/tooltip/faucet_tooltip.dart @@ -57,7 +57,12 @@ class _FaucetTooltipState extends State { color: const Color.fromRGBO(179, 240, 255, 1), // CDS에 없는 색상 child: Row( mainAxisSize: MainAxisSize.min, - children: [Text(widget.text, style: CoconutTypography.body3_12.setColor(CoconutColors.gray900))], + children: [ + MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.0)), + child: Text(widget.text, style: CoconutTypography.body3_12.setColor(CoconutColors.gray900)), + ), + ], ), ), ),