diff --git a/lib/core/utils/incoming_data_parser.dart b/lib/core/utils/incoming_data_parser.dart index 512c926..283430e 100644 --- a/lib/core/utils/incoming_data_parser.dart +++ b/lib/core/utils/incoming_data_parser.dart @@ -1,5 +1,7 @@ // Parser para detectar el tipo de dato entrante (QR, clipboard, etc.) -// Soporta: tokens Cashu (A/B), invoices Lightning, URLs de mint, payment requests +// Soporta: tokens Cashu (A/B), invoices Lightning, URLs de mint, payment requests, peanut emoji + +import 'peanut_codec.dart'; /// Modo de escaneo enum ScanMode { @@ -57,6 +59,15 @@ class IncomingDataParser { /// Detecta el tipo de dato y extrae información relevante static ParsedData parse(String data) { final trimmed = data.trim(); + + // Peanut-encoded token (🥜 + variation selectors) + if (PeanutCodec.isPeanut(trimmed)) { + final decoded = PeanutCodec.decode(trimmed); + if (decoded != null) { + return parse(decoded); + } + } + final lower = trimmed.toLowerCase(); // Token Cashu (cashuA... o cashuB...) diff --git a/lib/core/utils/peanut_codec.dart b/lib/core/utils/peanut_codec.dart new file mode 100644 index 0000000..a2db5f3 --- /dev/null +++ b/lib/core/utils/peanut_codec.dart @@ -0,0 +1,62 @@ +/// Peanut encoding/decoding for Cashu tokens. +/// +/// Encodes Cashu tokens into invisible Unicode Variation Selectors +/// appended to a 🥜 emoji, compatible with cashu.me's implementation. +/// +/// Each character of the base64 token is mapped to a Variation Selector: +/// - charCode 0-15 → VS1-VS16 (U+FE00 to U+FE0F) +/// - charCode 16-255 → VS17-VS256 (U+E0100 to U+E01EF) +class PeanutCodec { + static const String _peanutEmoji = '🥜'; + + /// Encodes a Cashu token string into peanut format (🥜 + variation selectors). + static String encode(String token) { + final buffer = StringBuffer(_peanutEmoji); + + for (int i = 0; i < token.length; i++) { + final byte = token.codeUnitAt(i); + + if (byte < 16) { + // VS1-VS16: U+FE00 to U+FE0F + buffer.writeCharCode(0xFE00 + byte); + } else if (byte < 256) { + // VS17-VS256: U+E0100 to U+E01EF + buffer.writeCharCode(0xE0100 + (byte - 16)); + } + } + + return buffer.toString(); + } + + /// Decodes a peanut-encoded string back to the original Cashu token. + /// Returns null if the input is not valid peanut format. + static String? decode(String peanut) { + final trimmed = peanut.trimLeft(); + if (!trimmed.startsWith(_peanutEmoji)) return null; + + final buffer = StringBuffer(); + final runes = trimmed.runes.toList(); + + // Skip the first rune (🥜 emoji) + for (int i = 1; i < runes.length; i++) { + final codePoint = runes[i]; + + if (codePoint >= 0xFE00 && codePoint <= 0xFE0F) { + // VS1-VS16 → byte 0-15 + buffer.writeCharCode(codePoint - 0xFE00); + } else if (codePoint >= 0xE0100 && codePoint <= 0xE01EF) { + // VS17-VS256 → byte 16-255 + buffer.writeCharCode(codePoint - 0xE0100 + 16); + } + // Skip any other characters (whitespace, etc.) + } + + final result = buffer.toString(); + return result.isNotEmpty ? result : null; + } + + /// Returns true if the string starts with the 🥜 emoji. + static bool isPeanut(String data) { + return data.trimLeft().startsWith(_peanutEmoji); + } +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f472923..8ee375d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.", "tokenCopiedToClipboard": "Token in Zwischenablage kopiert", + "copyAsEmoji": "Als Emoji kopieren", + "emojiCopiedToClipboard": "Token als Emoji kopiert 🥜", + "peanutDecodeError": "Emoji-Token konnte nicht dekodiert werden. Möglicherweise beschädigt.", "amountToDeposit": "Einzuzahlender Betrag:", "descriptionOptional": "Beschreibung (optional):", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d07ff58..93c9f81 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -112,6 +112,9 @@ "tokenCashuAnimatedQr": "Cashu Token (animated QR - {fragments} UR fragments)", "keepTokenWarning": "Keep this token until the recipient claims it. If you lose it, you will lose the funds.", "tokenCopiedToClipboard": "Token copied to clipboard", + "copyAsEmoji": "Copy as emoji", + "emojiCopiedToClipboard": "Token copied as emoji 🥜", + "peanutDecodeError": "Could not decode emoji token. It may be corrupted.", "amountToDeposit": "Amount to deposit:", "descriptionOptional": "Description (optional):", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index a2fe5fd..7f5bbb9 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.", "tokenCopiedToClipboard": "Token copiado al portapapeles", + "copyAsEmoji": "Copiar como emoji", + "emojiCopiedToClipboard": "Token copiado como emoji 🥜", + "peanutDecodeError": "No se pudo decodificar el token emoji. Puede estar corrupto.", "amountToDeposit": "Monto a depositar:", "descriptionOptional": "Descripción (opcional):", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8682927..99be06c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "Conservez ce token jusqu'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.", "tokenCopiedToClipboard": "Token copié dans le presse-papiers", + "copyAsEmoji": "Copier en emoji", + "emojiCopiedToClipboard": "Token copié en emoji 🥜", + "peanutDecodeError": "Impossible de décoder le token emoji. Il est peut-être corrompu.", "amountToDeposit": "Montant à déposer :", "descriptionOptional": "Description (optionnelle) :", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 55d6493..f63f355 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.", "tokenCopiedToClipboard": "Token copiato negli appunti", + "copyAsEmoji": "Copia come emoji", + "emojiCopiedToClipboard": "Token copiato come emoji 🥜", + "peanutDecodeError": "Impossibile decodificare il token emoji. Potrebbe essere corrotto.", "amountToDeposit": "Importo da depositare:", "descriptionOptional": "Descrizione (opzionale):", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index b229e34..eb89257 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "受取人が請求するまでこのトークンを保管してください。紛失すると資金を失います。", "tokenCopiedToClipboard": "トークンをクリップボードにコピーしました", + "copyAsEmoji": "絵文字としてコピー", + "emojiCopiedToClipboard": "トークンを絵文字としてコピーしました 🥜", + "peanutDecodeError": "絵文字トークンをデコードできませんでした。破損している可能性があります。", "amountToDeposit": "入金額:", "descriptionOptional": "説明(任意):", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 13933fb..5f661ad 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "수신자가 청구할 때까지 이 토큰을 보관하세요. 분실하면 자금을 잃게 됩니다.", "tokenCopiedToClipboard": "토큰이 클립보드에 복사되었습니다", + "copyAsEmoji": "이모지로 복사", + "emojiCopiedToClipboard": "토큰이 이모지로 복사되었습니다 🥜", + "peanutDecodeError": "이모지 토큰을 디코딩할 수 없습니다. 손상되었을 수 있습니다.", "amountToDeposit": "입금할 금액:", "descriptionOptional": "설명 (선택사항):", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 18f9e20..c2636fc 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -707,6 +707,24 @@ abstract class L10n { /// **'Token copiado al portapapeles'** String get tokenCopiedToClipboard; + /// No description provided for @copyAsEmoji. + /// + /// In es, this message translates to: + /// **'Copiar como emoji'** + String get copyAsEmoji; + + /// No description provided for @emojiCopiedToClipboard. + /// + /// In es, this message translates to: + /// **'Token copiado como emoji 🥜'** + String get emojiCopiedToClipboard; + + /// No description provided for @peanutDecodeError. + /// + /// In es, this message translates to: + /// **'No se pudo decodificar el token emoji. Puede estar corrupto.'** + String get peanutDecodeError; + /// No description provided for @amountToDeposit. /// /// In es, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1f418bf..dd56426 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -315,6 +315,15 @@ class L10nDe extends L10n { @override String get tokenCopiedToClipboard => 'Token in Zwischenablage kopiert'; + @override + String get copyAsEmoji => 'Als Emoji kopieren'; + + @override + String get emojiCopiedToClipboard => 'Token als Emoji kopiert 🥜'; + + @override + String get peanutDecodeError => 'Emoji-Token konnte nicht dekodiert werden. Möglicherweise beschädigt.'; + @override String get amountToDeposit => 'Einzuzahlender Betrag:'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f3f1bb1..af2374b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -315,6 +315,15 @@ class L10nEn extends L10n { @override String get tokenCopiedToClipboard => 'Token copied to clipboard'; + @override + String get copyAsEmoji => 'Copy as emoji'; + + @override + String get emojiCopiedToClipboard => 'Token copied as emoji 🥜'; + + @override + String get peanutDecodeError => 'Could not decode emoji token. It may be corrupted.'; + @override String get amountToDeposit => 'Amount to deposit:'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index bda81b2..b6b96e7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -315,6 +315,15 @@ class L10nEs extends L10n { @override String get tokenCopiedToClipboard => 'Token copiado al portapapeles'; + @override + String get copyAsEmoji => 'Copiar como emoji'; + + @override + String get emojiCopiedToClipboard => 'Token copiado como emoji 🥜'; + + @override + String get peanutDecodeError => 'No se pudo decodificar el token emoji. Puede estar corrupto.'; + @override String get amountToDeposit => 'Monto a depositar:'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 27b9527..df0bbce 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -315,6 +315,15 @@ class L10nFr extends L10n { @override String get tokenCopiedToClipboard => 'Token copié dans le presse-papiers'; + @override + String get copyAsEmoji => 'Copier en emoji'; + + @override + String get emojiCopiedToClipboard => 'Token copié en emoji 🥜'; + + @override + String get peanutDecodeError => 'Impossible de décoder le token emoji. Il est peut-être corrompu.'; + @override String get amountToDeposit => 'Montant à déposer :'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 269664a..bc46fac 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -315,6 +315,15 @@ class L10nIt extends L10n { @override String get tokenCopiedToClipboard => 'Token copiato negli appunti'; + @override + String get copyAsEmoji => 'Copia come emoji'; + + @override + String get emojiCopiedToClipboard => 'Token copiato come emoji 🥜'; + + @override + String get peanutDecodeError => 'Impossibile decodificare il token emoji. Potrebbe essere corrotto.'; + @override String get amountToDeposit => 'Importo da depositare:'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 4beaf77..d52a6ad 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -315,6 +315,15 @@ class L10nJa extends L10n { @override String get tokenCopiedToClipboard => 'トークンをクリップボードにコピーしました'; + @override + String get copyAsEmoji => '絵文字としてコピー'; + + @override + String get emojiCopiedToClipboard => 'トークンを絵文字としてコピーしました 🥜'; + + @override + String get peanutDecodeError => '絵文字トークンをデコードできませんでした。破損している可能性があります。'; + @override String get amountToDeposit => '入金額:'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 0a62422..4cf8d03 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -315,6 +315,15 @@ class L10nKo extends L10n { @override String get tokenCopiedToClipboard => '토큰이 클립보드에 복사되었습니다'; + @override + String get copyAsEmoji => '이모지로 복사'; + + @override + String get emojiCopiedToClipboard => '토큰이 이모지로 복사되었습니다 🥜'; + + @override + String get peanutDecodeError => '이모지 토큰을 디코딩할 수 없습니다. 손상되었을 수 있습니다.'; + @override String get amountToDeposit => '입금할 금액:'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 71f4519..5d625a0 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -315,6 +315,15 @@ class L10nPt extends L10n { @override String get tokenCopiedToClipboard => 'Token copiado para área de transferência'; + @override + String get copyAsEmoji => 'Copiar como emoji'; + + @override + String get emojiCopiedToClipboard => 'Token copiado como emoji 🥜'; + + @override + String get peanutDecodeError => 'Não foi possível decodificar o token emoji. Pode estar corrompido.'; + @override String get amountToDeposit => 'Valor a depositar:'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 284c35e..54669e3 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -315,6 +315,15 @@ class L10nRu extends L10n { @override String get tokenCopiedToClipboard => 'Токен скопирован в буфер обмена'; + @override + String get copyAsEmoji => 'Скопировать как эмодзи'; + + @override + String get emojiCopiedToClipboard => 'Токен скопирован как эмодзи 🥜'; + + @override + String get peanutDecodeError => 'Не удалось декодировать эмодзи-токен. Возможно, он повреждён.'; + @override String get amountToDeposit => 'Сумма для пополнения:'; diff --git a/lib/l10n/app_localizations_sw.dart b/lib/l10n/app_localizations_sw.dart index 0721266..f4e7129 100644 --- a/lib/l10n/app_localizations_sw.dart +++ b/lib/l10n/app_localizations_sw.dart @@ -315,6 +315,15 @@ class L10nSw extends L10n { @override String get tokenCopiedToClipboard => 'Tokeni imenakiliwa kwenye ubao'; + @override + String get copyAsEmoji => 'Nakili kama emoji'; + + @override + String get emojiCopiedToClipboard => 'Tokeni imenakiliwa kama emoji 🥜'; + + @override + String get peanutDecodeError => 'Imeshindwa kusimbua tokeni ya emoji. Inaweza kuwa imeharibika.'; + @override String get amountToDeposit => 'Kiasi cha kuweka:'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b3207ba..fe962cb 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -315,6 +315,15 @@ class L10nZh extends L10n { @override String get tokenCopiedToClipboard => '代币已复制到剪贴板'; + @override + String get copyAsEmoji => '复制为表情符号'; + + @override + String get emojiCopiedToClipboard => '代币已复制为表情符号 🥜'; + + @override + String get peanutDecodeError => '无法解码表情符号代币。可能已损坏。'; + @override String get amountToDeposit => '存入金额:'; diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 9509deb..28e3b9b 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.", "tokenCopiedToClipboard": "Token copiado para área de transferência", + "copyAsEmoji": "Copiar como emoji", + "emojiCopiedToClipboard": "Token copiado como emoji 🥜", + "peanutDecodeError": "Não foi possível decodificar o token emoji. Pode estar corrompido.", "amountToDeposit": "Valor a depositar:", "descriptionOptional": "Descrição (opcional):", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 7fe2684..f469f46 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.", "tokenCopiedToClipboard": "Токен скопирован в буфер обмена", + "copyAsEmoji": "Скопировать как эмодзи", + "emojiCopiedToClipboard": "Токен скопирован как эмодзи 🥜", + "peanutDecodeError": "Не удалось декодировать эмодзи-токен. Возможно, он повреждён.", "amountToDeposit": "Сумма для пополнения:", "descriptionOptional": "Описание (необязательно):", diff --git a/lib/l10n/app_sw.arb b/lib/l10n/app_sw.arb index 711aae9..3a1681b 100644 --- a/lib/l10n/app_sw.arb +++ b/lib/l10n/app_sw.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.", "tokenCopiedToClipboard": "Tokeni imenakiliwa kwenye ubao", + "copyAsEmoji": "Nakili kama emoji", + "emojiCopiedToClipboard": "Tokeni imenakiliwa kama emoji 🥜", + "peanutDecodeError": "Imeshindwa kusimbua tokeni ya emoji. Inaweza kuwa imeharibika.", "amountToDeposit": "Kiasi cha kuweka:", "descriptionOptional": "Maelezo (si lazima):", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f983fe3..52c5f12 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -137,6 +137,9 @@ }, "keepTokenWarning": "请保留此代币直到收款方领取。如果丢失,资金将无法找回。", "tokenCopiedToClipboard": "代币已复制到剪贴板", + "copyAsEmoji": "复制为表情符号", + "emojiCopiedToClipboard": "代币已复制为表情符号 🥜", + "peanutDecodeError": "无法解码表情符号代币。可能已损坏。", "amountToDeposit": "存入金额:", "descriptionOptional": "描述(可选):", diff --git a/lib/screens/4_receive/receive_screen.dart b/lib/screens/4_receive/receive_screen.dart index c8dd1d0..1fddaa4 100644 --- a/lib/screens/4_receive/receive_screen.dart +++ b/lib/screens/4_receive/receive_screen.dart @@ -8,6 +8,7 @@ import '../../core/constants/dimensions.dart'; import '../../core/utils/formatters.dart'; import '../../core/utils/incoming_data_parser.dart' hide TokenInfo; import '../../core/utils/nostr_utils.dart'; +import '../../core/utils/peanut_codec.dart'; import '../../widgets/common/gradient_background.dart'; import '../../widgets/common/glass_card.dart'; import '../../widgets/common/primary_button.dart'; @@ -779,9 +780,29 @@ class _ReceiveScreenState extends State { Future _pasteFromClipboard() async { final clipboardData = await Clipboard.getData(Clipboard.kTextPlain); + if (!mounted) return; if (clipboardData?.text != null) { - _tokenController.text = clipboardData!.text!.trim(); - _onTokenChanged(clipboardData.text!.trim()); + var text = clipboardData!.text!.trim(); + + // Auto-decode peanut emoji format + if (PeanutCodec.isPeanut(text)) { + final decoded = PeanutCodec.decode(text); + if (decoded != null) { + text = decoded; + } else { + // Malformed peanut: clear field and show specific error + _tokenController.clear(); + setState(() { + _isValidToken = false; + _tokenInfo = null; + _errorMessage = L10n.of(context)!.peanutDecodeError; + }); + return; + } + } + + _tokenController.text = text; + _onTokenChanged(text); } } @@ -789,6 +810,28 @@ class _ReceiveScreenState extends State { final walletProvider = context.read(); final p2pkProvider = context.read(); + // Auto-decode peanut emoji format + var tokenValue = value.trim(); + if (PeanutCodec.isPeanut(tokenValue)) { + final decoded = PeanutCodec.decode(tokenValue); + if (decoded != null) { + tokenValue = decoded; + // Update the text field with the decoded token + _tokenController.text = tokenValue; + _tokenController.selection = TextSelection.collapsed( + offset: tokenValue.length, + ); + } else { + // Malformed peanut: show specific error + setState(() { + _isValidToken = false; + _tokenInfo = null; + _errorMessage = L10n.of(context)!.peanutDecodeError; + }); + return; + } + } + setState(() { _errorMessage = null; _manualKeyError = null; @@ -799,24 +842,24 @@ class _ReceiveScreenState extends State { _lockedToPubkeyHex = null; _matchingKeyLabel = null; - if (value.isEmpty) { + if (tokenValue.isEmpty) { _isValidToken = false; _tokenInfo = null; return; } // Parsear token real con cdk-flutter - final tokenInfo = walletProvider.parseToken(value.trim()); + final tokenInfo = walletProvider.parseToken(tokenValue); if (tokenInfo != null) { _isValidToken = true; _tokenInfo = tokenInfo; // Detectar P2PK - _isP2PKLocked = p2pkProvider.isTokenLocked(value.trim()); + _isP2PKLocked = p2pkProvider.isTokenLocked(tokenValue); if (_isP2PKLocked) { - _lockedToPubkeyHex = p2pkProvider.extractLockedPubkey(value.trim()); - _isLockedToUs = p2pkProvider.isTokenLockedToUs(value.trim()); + _lockedToPubkeyHex = p2pkProvider.extractLockedPubkey(tokenValue); + _isLockedToUs = p2pkProvider.isTokenLockedToUs(tokenValue); // Si es nuestra, buscar el label de la clave if (_isLockedToUs && _lockedToPubkeyHex != null) { diff --git a/lib/screens/5_send/share_token_screen.dart b/lib/screens/5_send/share_token_screen.dart index 88e285f..bc6344e 100644 --- a/lib/screens/5_send/share_token_screen.dart +++ b/lib/screens/5_send/share_token_screen.dart @@ -9,6 +9,7 @@ import 'package:elcaju/l10n/app_localizations.dart'; import '../../core/constants/colors.dart'; import '../../core/constants/dimensions.dart'; import '../../core/utils/formatters.dart'; +import '../../core/utils/peanut_codec.dart'; import '../../widgets/common/gradient_background.dart'; import '../../widgets/common/glass_card.dart'; import '../../widgets/common/primary_button.dart'; @@ -414,7 +415,27 @@ class _ShareTokenScreenState extends State { ), ), ), - const SizedBox(width: AppDimensions.paddingMedium), + const SizedBox(width: AppDimensions.paddingSmall), + // Copiar como emoji (peanut encoding) + Tooltip( + message: L10n.of(context)!.copyAsEmoji, + child: GestureDetector( + onTap: () => _copyPeanut(context), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1, + ), + ), + child: Icon(LucideIcons.bean, color: AppColors.primaryAction, size: 22), + ), + ), + ), + const SizedBox(width: AppDimensions.paddingSmall), // Compartir Expanded( child: GestureDetector( @@ -492,6 +513,19 @@ class _ShareTokenScreenState extends State { ); } + void _copyPeanut(BuildContext context) { + final peanut = PeanutCodec.encode(widget.token); + Clipboard.setData(ClipboardData(text: peanut)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(L10n.of(context)!.emojiCopiedToClipboard), + backgroundColor: AppColors.success, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + void _shareToken(BuildContext context) async { final memo = widget.memo != null && widget.memo!.isNotEmpty ? '\n"${widget.memo}"'