Merge pull request #64 from Forte11Cuba/feature/peanut-emoji-encoding
feat: add peanut emoji encoding for Cashu tokens
This commit is contained in:
@@ -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...)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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):",
|
||||
|
||||
@@ -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):",
|
||||
|
||||
@@ -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):",
|
||||
|
||||
@@ -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) :",
|
||||
|
||||
@@ -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):",
|
||||
|
||||
@@ -137,6 +137,9 @@
|
||||
},
|
||||
"keepTokenWarning": "受取人が請求するまでこのトークンを保管してください。紛失すると資金を失います。",
|
||||
"tokenCopiedToClipboard": "トークンをクリップボードにコピーしました",
|
||||
"copyAsEmoji": "絵文字としてコピー",
|
||||
"emojiCopiedToClipboard": "トークンを絵文字としてコピーしました 🥜",
|
||||
"peanutDecodeError": "絵文字トークンをデコードできませんでした。破損している可能性があります。",
|
||||
|
||||
"amountToDeposit": "入金額:",
|
||||
"descriptionOptional": "説明(任意):",
|
||||
|
||||
@@ -137,6 +137,9 @@
|
||||
},
|
||||
"keepTokenWarning": "수신자가 청구할 때까지 이 토큰을 보관하세요. 분실하면 자금을 잃게 됩니다.",
|
||||
"tokenCopiedToClipboard": "토큰이 클립보드에 복사되었습니다",
|
||||
"copyAsEmoji": "이모지로 복사",
|
||||
"emojiCopiedToClipboard": "토큰이 이모지로 복사되었습니다 🥜",
|
||||
"peanutDecodeError": "이모지 토큰을 디코딩할 수 없습니다. 손상되었을 수 있습니다.",
|
||||
|
||||
"amountToDeposit": "입금할 금액:",
|
||||
"descriptionOptional": "설명 (선택사항):",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:';
|
||||
|
||||
|
||||
@@ -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:';
|
||||
|
||||
|
||||
@@ -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:';
|
||||
|
||||
|
||||
@@ -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 :';
|
||||
|
||||
|
||||
@@ -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:';
|
||||
|
||||
|
||||
@@ -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 => '入金額:';
|
||||
|
||||
|
||||
@@ -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 => '입금할 금액:';
|
||||
|
||||
|
||||
@@ -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:';
|
||||
|
||||
|
||||
@@ -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 => 'Сумма для пополнения:';
|
||||
|
||||
|
||||
@@ -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:';
|
||||
|
||||
|
||||
@@ -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 => '存入金额:';
|
||||
|
||||
|
||||
@@ -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):",
|
||||
|
||||
@@ -137,6 +137,9 @@
|
||||
},
|
||||
"keepTokenWarning": "Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.",
|
||||
"tokenCopiedToClipboard": "Токен скопирован в буфер обмена",
|
||||
"copyAsEmoji": "Скопировать как эмодзи",
|
||||
"emojiCopiedToClipboard": "Токен скопирован как эмодзи 🥜",
|
||||
"peanutDecodeError": "Не удалось декодировать эмодзи-токен. Возможно, он повреждён.",
|
||||
|
||||
"amountToDeposit": "Сумма для пополнения:",
|
||||
"descriptionOptional": "Описание (необязательно):",
|
||||
|
||||
@@ -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):",
|
||||
|
||||
@@ -137,6 +137,9 @@
|
||||
},
|
||||
"keepTokenWarning": "请保留此代币直到收款方领取。如果丢失,资金将无法找回。",
|
||||
"tokenCopiedToClipboard": "代币已复制到剪贴板",
|
||||
"copyAsEmoji": "复制为表情符号",
|
||||
"emojiCopiedToClipboard": "代币已复制为表情符号 🥜",
|
||||
"peanutDecodeError": "无法解码表情符号代币。可能已损坏。",
|
||||
|
||||
"amountToDeposit": "存入金额:",
|
||||
"descriptionOptional": "描述(可选):",
|
||||
|
||||
@@ -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<ReceiveScreen> {
|
||||
|
||||
Future<void> _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<ReceiveScreen> {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final p2pkProvider = context.read<P2PKProvider>();
|
||||
|
||||
// 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<ReceiveScreen> {
|
||||
_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) {
|
||||
|
||||
@@ -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<ShareTokenScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
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<ShareTokenScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
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}"'
|
||||
|
||||
Reference in New Issue
Block a user