diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 9d61506..6a51e15 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -367,6 +367,8 @@ "nameHint": "How others see you", "about": "About", "aboutHint": "A short line — what you grow, where", + "g1": "Ğ1 address (optional)", + "g1Hint": "So people can pay you in Ğ1 — separate from your key", "yourId": "Your identity", "idHelp": "Share this so people can recognise you", "copy": "Copy", @@ -383,7 +385,9 @@ "hint": "Write a message…", "send": "Send", "empty": "No messages yet — say hello", - "offline": "Set up sharing to send messages" + "offline": "Set up sharing to send messages", + "payG1": "Pay in Ğ1", + "g1Copied": "Ğ1 address copied — paste it in your wallet" }, "trust": { "none": "No one vouches for them yet", diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 8957e1b..427207b 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -367,6 +367,8 @@ "nameHint": "Cómo te ven los demás", "about": "Sobre ti", "aboutHint": "Una línea — qué cultivas, dónde", + "g1": "Dirección Ğ1 (opcional)", + "g1Hint": "Para que te paguen en Ğ1 — aparte de tu clave", "yourId": "Tu identidad", "idHelp": "Compártela para que te reconozcan", "copy": "Copiar", @@ -383,7 +385,9 @@ "hint": "Escribe un mensaje…", "send": "Enviar", "empty": "Aún no hay mensajes — saluda", - "offline": "Configura el compartir para enviar mensajes" + "offline": "Configura el compartir para enviar mensajes", + "payG1": "Pagar en Ğ1", + "g1Copied": "Dirección Ğ1 copiada — pégala en tu cartera" }, "trust": { "none": "Nadie los avala aún", diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index 20408da..58cd6c5 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -363,6 +363,8 @@ "nameHint": "Como os outros te veem", "about": "Sobre ti", "aboutHint": "Uma linha — o que cultivas, onde", + "g1": "Endereço Ğ1 (opcional)", + "g1Hint": "Para que te paguem em Ğ1 — separado da tua chave", "yourId": "A tua identidade", "idHelp": "Partilha-a para que te reconheçam", "copy": "Copiar", @@ -379,7 +381,9 @@ "hint": "Escreve uma mensagem…", "send": "Enviar", "empty": "Ainda não há mensagens — diz olá", - "offline": "Configura a partilha para enviar mensagens" + "offline": "Configura a partilha para enviar mensagens", + "payG1": "Pagar em Ğ1", + "g1Copied": "Endereço Ğ1 copiado — cola-o na tua carteira" }, "trust": { "none": "Ainda ninguém os avaliza", diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index b2cca1f..53d10f1 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 3 -/// Strings: 1031 (343 per locale) +/// Strings: 1043 (347 per locale) /// -/// Built on 2026-07-10 at 10:36 UTC +/// Built on 2026-07-10 at 11:05 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index 747b95f..ff9374e 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -1167,6 +1167,12 @@ class Translations$profile$en { /// en: 'A short line — what you grow, where' String get aboutHint => 'A short line — what you grow, where'; + /// en: 'Ğ1 address (optional)' + String get g1 => 'Ğ1 address (optional)'; + + /// en: 'So people can pay you in Ğ1 — separate from your key' + String get g1Hint => 'So people can pay you in Ğ1 — separate from your key'; + /// en: 'Your identity' String get yourId => 'Your identity'; @@ -1223,6 +1229,12 @@ class Translations$chat$en { /// en: 'Set up sharing to send messages' String get offline => 'Set up sharing to send messages'; + + /// en: 'Pay in Ğ1' + String get payG1 => 'Pay in Ğ1'; + + /// en: 'Ğ1 address copied — paste it in your wallet' + String get g1Copied => 'Ğ1 address copied — paste it in your wallet'; } // Path: trust @@ -2033,6 +2045,8 @@ extension on Translations { 'profile.nameHint' => 'How others see you', 'profile.about' => 'About', 'profile.aboutHint' => 'A short line — what you grow, where', + 'profile.g1' => 'Ğ1 address (optional)', + 'profile.g1Hint' => 'So people can pay you in Ğ1 — separate from your key', 'profile.yourId' => 'Your identity', 'profile.idHelp' => 'Share this so people can recognise you', 'profile.copy' => 'Copy', @@ -2046,6 +2060,8 @@ extension on Translations { 'chat.send' => 'Send', 'chat.empty' => 'No messages yet — say hello', 'chat.offline' => 'Set up sharing to send messages', + 'chat.payG1' => 'Pay in Ğ1', + 'chat.g1Copied' => 'Ğ1 address copied — paste it in your wallet', 'trust.none' => 'No one vouches for them yet', 'trust.count' => ({required Object n}) => 'Vouched for by ${n}', 'trust.vouch' => 'I know this person', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index a18ecd9..5aa5b87 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -648,6 +648,8 @@ class _Translations$profile$es extends Translations$profile$en { @override String get nameHint => 'Cómo te ven los demás'; @override String get about => 'Sobre ti'; @override String get aboutHint => 'Una línea — qué cultivas, dónde'; + @override String get g1 => 'Dirección Ğ1 (opcional)'; + @override String get g1Hint => 'Para que te paguen en Ğ1 — aparte de tu clave'; @override String get yourId => 'Tu identidad'; @override String get idHelp => 'Compártela para que te reconozcan'; @override String get copy => 'Copiar'; @@ -679,6 +681,8 @@ class _Translations$chat$es extends Translations$chat$en { @override String get send => 'Enviar'; @override String get empty => 'Aún no hay mensajes — saluda'; @override String get offline => 'Configura el compartir para enviar mensajes'; + @override String get payG1 => 'Pagar en Ğ1'; + @override String get g1Copied => 'Dirección Ğ1 copiada — pégala en tu cartera'; } // Path: trust @@ -1363,6 +1367,8 @@ extension on TranslationsEs { 'profile.nameHint' => 'Cómo te ven los demás', 'profile.about' => 'Sobre ti', 'profile.aboutHint' => 'Una línea — qué cultivas, dónde', + 'profile.g1' => 'Dirección Ğ1 (opcional)', + 'profile.g1Hint' => 'Para que te paguen en Ğ1 — aparte de tu clave', 'profile.yourId' => 'Tu identidad', 'profile.idHelp' => 'Compártela para que te reconozcan', 'profile.copy' => 'Copiar', @@ -1376,6 +1382,8 @@ extension on TranslationsEs { 'chat.send' => 'Enviar', 'chat.empty' => 'Aún no hay mensajes — saluda', 'chat.offline' => 'Configura el compartir para enviar mensajes', + 'chat.payG1' => 'Pagar en Ğ1', + 'chat.g1Copied' => 'Dirección Ğ1 copiada — pégala en tu cartera', 'trust.none' => 'Nadie los avala aún', 'trust.count' => ({required Object n}) => 'Avalada por ${n}', 'trust.vouch' => 'Conozco a esta persona', diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index 882f447..a04c438 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -644,6 +644,8 @@ class _Translations$profile$pt extends Translations$profile$en { @override String get nameHint => 'Como os outros te veem'; @override String get about => 'Sobre ti'; @override String get aboutHint => 'Uma linha — o que cultivas, onde'; + @override String get g1 => 'Endereço Ğ1 (opcional)'; + @override String get g1Hint => 'Para que te paguem em Ğ1 — separado da tua chave'; @override String get yourId => 'A tua identidade'; @override String get idHelp => 'Partilha-a para que te reconheçam'; @override String get copy => 'Copiar'; @@ -675,6 +677,8 @@ class _Translations$chat$pt extends Translations$chat$en { @override String get send => 'Enviar'; @override String get empty => 'Ainda não há mensagens — diz olá'; @override String get offline => 'Configura a partilha para enviar mensagens'; + @override String get payG1 => 'Pagar em Ğ1'; + @override String get g1Copied => 'Endereço Ğ1 copiado — cola-o na tua carteira'; } // Path: trust @@ -1355,6 +1359,8 @@ extension on TranslationsPt { 'profile.nameHint' => 'Como os outros te veem', 'profile.about' => 'Sobre ti', 'profile.aboutHint' => 'Uma linha — o que cultivas, onde', + 'profile.g1' => 'Endereço Ğ1 (opcional)', + 'profile.g1Hint' => 'Para que te paguem em Ğ1 — separado da tua chave', 'profile.yourId' => 'A tua identidade', 'profile.idHelp' => 'Partilha-a para que te reconheçam', 'profile.copy' => 'Copiar', @@ -1368,6 +1374,8 @@ extension on TranslationsPt { 'chat.send' => 'Enviar', 'chat.empty' => 'Ainda não há mensagens — diz olá', 'chat.offline' => 'Configura a partilha para enviar mensagens', + 'chat.payG1' => 'Pagar em Ğ1', + 'chat.g1Copied' => 'Endereço Ğ1 copiado — cola-o na tua carteira', 'trust.none' => 'Ainda ninguém os avaliza', 'trust.count' => ({required Object n}) => 'Avalizada por ${n}', 'trust.vouch' => 'Conheço esta pessoa', diff --git a/apps/app_seeds/lib/services/profile_store.dart b/apps/app_seeds/lib/services/profile_store.dart index 99e2660..d99c4f4 100644 --- a/apps/app_seeds/lib/services/profile_store.dart +++ b/apps/app_seeds/lib/services/profile_store.dart @@ -9,12 +9,21 @@ class ProfileStore { final SecretStore _store; static const _nameKey = 'tane.social.profile.name'; static const _aboutKey = 'tane.social.profile.about'; + static const _g1Key = 'tane.social.profile.g1'; Future name() async => (await _store.read(_nameKey)) ?? ''; Future about() async => (await _store.read(_aboutKey)) ?? ''; - Future save({required String name, required String about}) async { + /// Your Ğ1 (Duniter) address, if you chose to share one. + Future g1() async => (await _store.read(_g1Key)) ?? ''; + + Future save({ + required String name, + required String about, + String g1 = '', + }) async { await _store.write(_nameKey, name.trim()); await _store.write(_aboutKey, about.trim()); + await _store.write(_g1Key, g1.trim()); } } diff --git a/apps/app_seeds/lib/ui/chat_screen.dart b/apps/app_seeds/lib/ui/chat_screen.dart index a618656..97a555c 100644 --- a/apps/app_seeds/lib/ui/chat_screen.dart +++ b/apps/app_seeds/lib/ui/chat_screen.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../i18n/strings.g.dart'; import '../services/message_store.dart'; @@ -45,6 +47,7 @@ class _ChatScreenState extends State { TrustCubit? _trust; bool _loading = true; String? _peerName; + String? _peerG1; final _input = TextEditingController(); @override @@ -94,9 +97,15 @@ class _ChatScreenState extends State { unawaited(() async { try { final profile = await live.profile.fetch(widget.peerPubkey); - if (profile != null && profile.name.isNotEmpty && mounted) { - await widget.profileCache!.setName(widget.peerPubkey, profile.name); - setState(() => _peerName = profile.name); + if (profile != null && mounted) { + if (profile.name.isNotEmpty) { + await widget.profileCache! + .setName(widget.peerPubkey, profile.name); + } + setState(() { + if (profile.name.isNotEmpty) _peerName = profile.name; + if (profile.g1.isNotEmpty) _peerG1 = profile.g1; + }); } } catch (_) { // best effort @@ -105,6 +114,30 @@ class _ChatScreenState extends State { } } + /// Opens the peer's Ğ1 wallet so you can pay them (Ğ1 is separate from the + /// Nostr key — this uses the address they published in their profile). Falls + /// back to copying the address if no wallet handles the link. + /// + /// NOTE: confirm the deep-link scheme for the target wallet (Ğ1nkgo / Ğecko / + /// Cesium) — this uses the Cesium web wallet as a universal fallback. + Future _payG1() async { + final g1 = _peerG1; + if (g1 == null) return; + final messenger = ScaffoldMessenger.of(context); + final t = context.t; + final uri = Uri.parse('https://demo.cesium.app/#/app/wallet/$g1/'); + var launched = false; + try { + launched = await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (_) { + launched = false; + } + if (!launched && mounted) { + await Clipboard.setData(ClipboardData(text: g1)); + messenger.showSnackBar(SnackBar(content: Text(t.chat.g1Copied))); + } + } + Future _send() async { final cubit = _messages; if (cubit == null || _input.text.trim().isEmpty) return; @@ -126,9 +159,19 @@ class _ChatScreenState extends State { Widget build(BuildContext context) { final messages = _messages; final trust = _trust; + final t = context.t; return Scaffold( appBar: AppBar( title: Text(_peerName ?? shortPubkey(widget.peerPubkey)), + actions: [ + if (_peerG1 != null) + IconButton( + key: const Key('chat.payG1'), + icon: const Icon(Icons.payments_outlined), + tooltip: t.chat.payG1, + onPressed: _payG1, + ), + ], ), body: _loading || messages == null || trust == null ? const Center(child: CircularProgressIndicator()) diff --git a/apps/app_seeds/lib/ui/profile_screen.dart b/apps/app_seeds/lib/ui/profile_screen.dart index 56f0141..815671d 100644 --- a/apps/app_seeds/lib/ui/profile_screen.dart +++ b/apps/app_seeds/lib/ui/profile_screen.dart @@ -30,6 +30,7 @@ class ProfileScreen extends StatefulWidget { class _ProfileScreenState extends State { final _name = TextEditingController(); final _about = TextEditingController(); + final _g1 = TextEditingController(); bool _loading = true; bool _saving = false; @@ -42,6 +43,7 @@ class _ProfileScreenState extends State { Future _load() async { _name.text = await widget.profileStore.name(); _about.text = await widget.profileStore.about(); + _g1.text = await widget.profileStore.g1(); if (mounted) setState(() => _loading = false); } @@ -57,7 +59,8 @@ class _ProfileScreenState extends State { final messenger = ScaffoldMessenger.of(context); final t = context.t; setState(() => _saving = true); - await widget.profileStore.save(name: _name.text, about: _about.text); + await widget.profileStore + .save(name: _name.text, about: _about.text, g1: _g1.text); // Best-effort publish to the network so peers see the name. final relays = await widget.settings.relayUrls(); @@ -67,6 +70,7 @@ class _ProfileScreenState extends State { await session.profile.publish( name: _name.text.trim(), about: _about.text.trim(), + g1: _g1.text.trim(), ); await session.close(); } catch (_) { @@ -82,6 +86,7 @@ class _ProfileScreenState extends State { void dispose() { _name.dispose(); _about.dispose(); + _g1.dispose(); super.dispose(); } @@ -116,6 +121,16 @@ class _ProfileScreenState extends State { helperText: t.profile.aboutHint, ), ), + const SizedBox(height: 16), + TextField( + key: const Key('profile.g1'), + controller: _g1, + decoration: InputDecoration( + labelText: t.profile.g1, + helperText: t.profile.g1Hint, + helperMaxLines: 2, + ), + ), const SizedBox(height: 24), FilledButton( key: const Key('profile.save'), diff --git a/packages/commons_core/lib/src/social/nostr/nostr_profile_transport.dart b/packages/commons_core/lib/src/social/nostr/nostr_profile_transport.dart index 62abd79..c2e5e69 100644 --- a/packages/commons_core/lib/src/social/nostr/nostr_profile_transport.dart +++ b/packages/commons_core/lib/src/social/nostr/nostr_profile_transport.dart @@ -19,11 +19,13 @@ class NostrProfileTransport implements ProfileTransport { String name = '', String about = '', String picture = '', + String g1 = '', }) async { final content = jsonEncode({ if (name.isNotEmpty) 'name': name, if (about.isNotEmpty) 'about': about, if (picture.isNotEmpty) 'picture': picture, + if (g1.isNotEmpty) 'g1': g1, }); final event = Event.from( kind: kindMetadata, @@ -50,6 +52,7 @@ class NostrProfileTransport implements ProfileTransport { name: (map['name'] as String?) ?? '', about: (map['about'] as String?) ?? '', picture: (map['picture'] as String?) ?? '', + g1: (map['g1'] as String?) ?? '', ); } catch (_) { return UserProfile(pubkeyHex: pubkeyHex); diff --git a/packages/commons_core/lib/src/social/profile_transport.dart b/packages/commons_core/lib/src/social/profile_transport.dart index 961ad54..3cc9617 100644 --- a/packages/commons_core/lib/src/social/profile_transport.dart +++ b/packages/commons_core/lib/src/social/profile_transport.dart @@ -6,6 +6,7 @@ class UserProfile { this.name = '', this.about = '', this.picture = '', + this.g1 = '', }); final String pubkeyHex; @@ -13,7 +14,12 @@ class UserProfile { final String about; final String picture; - bool get isEmpty => name.isEmpty && about.isEmpty && picture.isEmpty; + /// Optional Ğ1 (Duniter) address the person chose to publish, so peers can pay + /// them in Ğ1. Separate from the Nostr key; empty when not shared. + final String g1; + + bool get isEmpty => + name.isEmpty && about.isEmpty && picture.isEmpty && g1.isEmpty; } /// Publish/fetch public profile metadata. Kind:0 is replaceable — publishing @@ -21,7 +27,7 @@ class UserProfile { /// sibling of the offer/message/trust transports over the same connection. abstract interface class ProfileTransport { /// Publishes (replaces) this identity's profile. - Future publish({String name, String about, String picture}); + Future publish({String name, String about, String picture, String g1}); /// Fetches [pubkeyHex]'s latest profile, or null if they never published one. Future fetch(String pubkeyHex); diff --git a/packages/commons_core/test/social/nostr_profile_test.dart b/packages/commons_core/test/social/nostr_profile_test.dart index 49c95ec..9b40ccf 100644 --- a/packages/commons_core/test/social/nostr_profile_test.dart +++ b/packages/commons_core/test/social/nostr_profile_test.dart @@ -16,8 +16,11 @@ void main() { test('publish then fetch round-trips a profile (kind:0)', () async { final alice = await idFor(1); final aliceConn = await NostrConnection.connect(relay.url, identity: alice); - await NostrProfileTransport(aliceConn) - .publish(name: 'Alicia', about: 'Guardo tomate rosa'); + await NostrProfileTransport(aliceConn).publish( + name: 'Alicia', + about: 'Guardo tomate rosa', + g1: 'G1abc123', + ); // Anyone else can fetch it. final bob = await idFor(2); @@ -28,6 +31,7 @@ void main() { expect(fetched, isNotNull); expect(fetched!.name, 'Alicia'); expect(fetched.about, 'Guardo tomate rosa'); + expect(fetched.g1, 'G1abc123'); await aliceConn.close(); await bobConn.close();