feat(block2): Ğ1 — publish a Ğ1 address + 'Pay in Ğ1' in chat (levels 1-2)
The optional Ğ1 (free currency) integration, per g1-integration.md. - Profile: an optional 'Ğ1 address' field, persisted locally and published in your NIP-01 kind:0 metadata (separate from the Nostr key). - commons_core: UserProfile/ProfileTransport carry a 'g1' field (round-trip tested). - Chat: when the peer published a Ğ1 address, a 'Pay in Ğ1' app-bar action opens their wallet (url_launcher) so value moves in the wallet, not the app — copies the address as a fallback. No payment rails in-app (sharing-model §4.3). NOTE (vjrj): the wallet deep-link uses the Cesium web wallet as a universal fallback; swap the URI in chat_screen._payG1 for the Ğ1nkgo/Ğecko scheme you prefer. Analyzer clean (both packages); profile transport test covers g1.
This commit is contained in:
parent
3067d9ca7f
commit
a9c6030dde
13 changed files with 138 additions and 14 deletions
|
|
@ -367,6 +367,8 @@
|
||||||
"nameHint": "How others see you",
|
"nameHint": "How others see you",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"aboutHint": "A short line — what you grow, where",
|
"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",
|
"yourId": "Your identity",
|
||||||
"idHelp": "Share this so people can recognise you",
|
"idHelp": "Share this so people can recognise you",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
|
@ -383,7 +385,9 @@
|
||||||
"hint": "Write a message…",
|
"hint": "Write a message…",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"empty": "No messages yet — say hello",
|
"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": {
|
"trust": {
|
||||||
"none": "No one vouches for them yet",
|
"none": "No one vouches for them yet",
|
||||||
|
|
|
||||||
|
|
@ -367,6 +367,8 @@
|
||||||
"nameHint": "Cómo te ven los demás",
|
"nameHint": "Cómo te ven los demás",
|
||||||
"about": "Sobre ti",
|
"about": "Sobre ti",
|
||||||
"aboutHint": "Una línea — qué cultivas, dónde",
|
"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",
|
"yourId": "Tu identidad",
|
||||||
"idHelp": "Compártela para que te reconozcan",
|
"idHelp": "Compártela para que te reconozcan",
|
||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
|
|
@ -383,7 +385,9 @@
|
||||||
"hint": "Escribe un mensaje…",
|
"hint": "Escribe un mensaje…",
|
||||||
"send": "Enviar",
|
"send": "Enviar",
|
||||||
"empty": "Aún no hay mensajes — saluda",
|
"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": {
|
"trust": {
|
||||||
"none": "Nadie los avala aún",
|
"none": "Nadie los avala aún",
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,8 @@
|
||||||
"nameHint": "Como os outros te veem",
|
"nameHint": "Como os outros te veem",
|
||||||
"about": "Sobre ti",
|
"about": "Sobre ti",
|
||||||
"aboutHint": "Uma linha — o que cultivas, onde",
|
"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",
|
"yourId": "A tua identidade",
|
||||||
"idHelp": "Partilha-a para que te reconheçam",
|
"idHelp": "Partilha-a para que te reconheçam",
|
||||||
"copy": "Copiar",
|
"copy": "Copiar",
|
||||||
|
|
@ -379,7 +381,9 @@
|
||||||
"hint": "Escreve uma mensagem…",
|
"hint": "Escreve uma mensagem…",
|
||||||
"send": "Enviar",
|
"send": "Enviar",
|
||||||
"empty": "Ainda não há mensagens — diz olá",
|
"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": {
|
"trust": {
|
||||||
"none": "Ainda ninguém os avaliza",
|
"none": "Ainda ninguém os avaliza",
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 3
|
/// 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
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|
|
||||||
|
|
@ -1167,6 +1167,12 @@ class Translations$profile$en {
|
||||||
/// en: 'A short line — what you grow, where'
|
/// en: 'A short line — what you grow, where'
|
||||||
String get aboutHint => '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'
|
/// en: 'Your identity'
|
||||||
String get yourId => 'Your identity';
|
String get yourId => 'Your identity';
|
||||||
|
|
||||||
|
|
@ -1223,6 +1229,12 @@ class Translations$chat$en {
|
||||||
|
|
||||||
/// en: 'Set up sharing to send messages'
|
/// en: 'Set up sharing to send messages'
|
||||||
String get offline => '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
|
// Path: trust
|
||||||
|
|
@ -2033,6 +2045,8 @@ extension on Translations {
|
||||||
'profile.nameHint' => 'How others see you',
|
'profile.nameHint' => 'How others see you',
|
||||||
'profile.about' => 'About',
|
'profile.about' => 'About',
|
||||||
'profile.aboutHint' => 'A short line — what you grow, where',
|
'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.yourId' => 'Your identity',
|
||||||
'profile.idHelp' => 'Share this so people can recognise you',
|
'profile.idHelp' => 'Share this so people can recognise you',
|
||||||
'profile.copy' => 'Copy',
|
'profile.copy' => 'Copy',
|
||||||
|
|
@ -2046,6 +2060,8 @@ extension on Translations {
|
||||||
'chat.send' => 'Send',
|
'chat.send' => 'Send',
|
||||||
'chat.empty' => 'No messages yet — say hello',
|
'chat.empty' => 'No messages yet — say hello',
|
||||||
'chat.offline' => 'Set up sharing to send messages',
|
'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.none' => 'No one vouches for them yet',
|
||||||
'trust.count' => ({required Object n}) => 'Vouched for by ${n}',
|
'trust.count' => ({required Object n}) => 'Vouched for by ${n}',
|
||||||
'trust.vouch' => 'I know this person',
|
'trust.vouch' => 'I know this person',
|
||||||
|
|
|
||||||
|
|
@ -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 nameHint => 'Cómo te ven los demás';
|
||||||
@override String get about => 'Sobre ti';
|
@override String get about => 'Sobre ti';
|
||||||
@override String get aboutHint => 'Una línea — qué cultivas, dónde';
|
@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 yourId => 'Tu identidad';
|
||||||
@override String get idHelp => 'Compártela para que te reconozcan';
|
@override String get idHelp => 'Compártela para que te reconozcan';
|
||||||
@override String get copy => 'Copiar';
|
@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 send => 'Enviar';
|
||||||
@override String get empty => 'Aún no hay mensajes — saluda';
|
@override String get empty => 'Aún no hay mensajes — saluda';
|
||||||
@override String get offline => 'Configura el compartir para enviar mensajes';
|
@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
|
// Path: trust
|
||||||
|
|
@ -1363,6 +1367,8 @@ extension on TranslationsEs {
|
||||||
'profile.nameHint' => 'Cómo te ven los demás',
|
'profile.nameHint' => 'Cómo te ven los demás',
|
||||||
'profile.about' => 'Sobre ti',
|
'profile.about' => 'Sobre ti',
|
||||||
'profile.aboutHint' => 'Una línea — qué cultivas, dónde',
|
'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.yourId' => 'Tu identidad',
|
||||||
'profile.idHelp' => 'Compártela para que te reconozcan',
|
'profile.idHelp' => 'Compártela para que te reconozcan',
|
||||||
'profile.copy' => 'Copiar',
|
'profile.copy' => 'Copiar',
|
||||||
|
|
@ -1376,6 +1382,8 @@ extension on TranslationsEs {
|
||||||
'chat.send' => 'Enviar',
|
'chat.send' => 'Enviar',
|
||||||
'chat.empty' => 'Aún no hay mensajes — saluda',
|
'chat.empty' => 'Aún no hay mensajes — saluda',
|
||||||
'chat.offline' => 'Configura el compartir para enviar mensajes',
|
'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.none' => 'Nadie los avala aún',
|
||||||
'trust.count' => ({required Object n}) => 'Avalada por ${n}',
|
'trust.count' => ({required Object n}) => 'Avalada por ${n}',
|
||||||
'trust.vouch' => 'Conozco a esta persona',
|
'trust.vouch' => 'Conozco a esta persona',
|
||||||
|
|
|
||||||
|
|
@ -644,6 +644,8 @@ class _Translations$profile$pt extends Translations$profile$en {
|
||||||
@override String get nameHint => 'Como os outros te veem';
|
@override String get nameHint => 'Como os outros te veem';
|
||||||
@override String get about => 'Sobre ti';
|
@override String get about => 'Sobre ti';
|
||||||
@override String get aboutHint => 'Uma linha — o que cultivas, onde';
|
@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 yourId => 'A tua identidade';
|
||||||
@override String get idHelp => 'Partilha-a para que te reconheçam';
|
@override String get idHelp => 'Partilha-a para que te reconheçam';
|
||||||
@override String get copy => 'Copiar';
|
@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 send => 'Enviar';
|
||||||
@override String get empty => 'Ainda não há mensagens — diz olá';
|
@override String get empty => 'Ainda não há mensagens — diz olá';
|
||||||
@override String get offline => 'Configura a partilha para enviar mensagens';
|
@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
|
// Path: trust
|
||||||
|
|
@ -1355,6 +1359,8 @@ extension on TranslationsPt {
|
||||||
'profile.nameHint' => 'Como os outros te veem',
|
'profile.nameHint' => 'Como os outros te veem',
|
||||||
'profile.about' => 'Sobre ti',
|
'profile.about' => 'Sobre ti',
|
||||||
'profile.aboutHint' => 'Uma linha — o que cultivas, onde',
|
'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.yourId' => 'A tua identidade',
|
||||||
'profile.idHelp' => 'Partilha-a para que te reconheçam',
|
'profile.idHelp' => 'Partilha-a para que te reconheçam',
|
||||||
'profile.copy' => 'Copiar',
|
'profile.copy' => 'Copiar',
|
||||||
|
|
@ -1368,6 +1374,8 @@ extension on TranslationsPt {
|
||||||
'chat.send' => 'Enviar',
|
'chat.send' => 'Enviar',
|
||||||
'chat.empty' => 'Ainda não há mensagens — diz olá',
|
'chat.empty' => 'Ainda não há mensagens — diz olá',
|
||||||
'chat.offline' => 'Configura a partilha para enviar mensagens',
|
'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.none' => 'Ainda ninguém os avaliza',
|
||||||
'trust.count' => ({required Object n}) => 'Avalizada por ${n}',
|
'trust.count' => ({required Object n}) => 'Avalizada por ${n}',
|
||||||
'trust.vouch' => 'Conheço esta pessoa',
|
'trust.vouch' => 'Conheço esta pessoa',
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,21 @@ class ProfileStore {
|
||||||
final SecretStore _store;
|
final SecretStore _store;
|
||||||
static const _nameKey = 'tane.social.profile.name';
|
static const _nameKey = 'tane.social.profile.name';
|
||||||
static const _aboutKey = 'tane.social.profile.about';
|
static const _aboutKey = 'tane.social.profile.about';
|
||||||
|
static const _g1Key = 'tane.social.profile.g1';
|
||||||
|
|
||||||
Future<String> name() async => (await _store.read(_nameKey)) ?? '';
|
Future<String> name() async => (await _store.read(_nameKey)) ?? '';
|
||||||
Future<String> about() async => (await _store.read(_aboutKey)) ?? '';
|
Future<String> about() async => (await _store.read(_aboutKey)) ?? '';
|
||||||
|
|
||||||
Future<void> save({required String name, required String about}) async {
|
/// Your Ğ1 (Duniter) address, if you chose to share one.
|
||||||
|
Future<String> g1() async => (await _store.read(_g1Key)) ?? '';
|
||||||
|
|
||||||
|
Future<void> save({
|
||||||
|
required String name,
|
||||||
|
required String about,
|
||||||
|
String g1 = '',
|
||||||
|
}) async {
|
||||||
await _store.write(_nameKey, name.trim());
|
await _store.write(_nameKey, name.trim());
|
||||||
await _store.write(_aboutKey, about.trim());
|
await _store.write(_aboutKey, about.trim());
|
||||||
|
await _store.write(_g1Key, g1.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../i18n/strings.g.dart';
|
import '../i18n/strings.g.dart';
|
||||||
import '../services/message_store.dart';
|
import '../services/message_store.dart';
|
||||||
|
|
@ -45,6 +47,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
TrustCubit? _trust;
|
TrustCubit? _trust;
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
String? _peerName;
|
String? _peerName;
|
||||||
|
String? _peerG1;
|
||||||
final _input = TextEditingController();
|
final _input = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -94,9 +97,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
unawaited(() async {
|
unawaited(() async {
|
||||||
try {
|
try {
|
||||||
final profile = await live.profile.fetch(widget.peerPubkey);
|
final profile = await live.profile.fetch(widget.peerPubkey);
|
||||||
if (profile != null && profile.name.isNotEmpty && mounted) {
|
if (profile != null && mounted) {
|
||||||
await widget.profileCache!.setName(widget.peerPubkey, profile.name);
|
if (profile.name.isNotEmpty) {
|
||||||
setState(() => _peerName = profile.name);
|
await widget.profileCache!
|
||||||
|
.setName(widget.peerPubkey, profile.name);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
if (profile.name.isNotEmpty) _peerName = profile.name;
|
||||||
|
if (profile.g1.isNotEmpty) _peerG1 = profile.g1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// best effort
|
// best effort
|
||||||
|
|
@ -105,6 +114,30 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<void> _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<void> _send() async {
|
Future<void> _send() async {
|
||||||
final cubit = _messages;
|
final cubit = _messages;
|
||||||
if (cubit == null || _input.text.trim().isEmpty) return;
|
if (cubit == null || _input.text.trim().isEmpty) return;
|
||||||
|
|
@ -126,9 +159,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final messages = _messages;
|
final messages = _messages;
|
||||||
final trust = _trust;
|
final trust = _trust;
|
||||||
|
final t = context.t;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(_peerName ?? shortPubkey(widget.peerPubkey)),
|
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
|
body: _loading || messages == null || trust == null
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ class ProfileScreen extends StatefulWidget {
|
||||||
class _ProfileScreenState extends State<ProfileScreen> {
|
class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
final _name = TextEditingController();
|
final _name = TextEditingController();
|
||||||
final _about = TextEditingController();
|
final _about = TextEditingController();
|
||||||
|
final _g1 = TextEditingController();
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
bool _saving = false;
|
bool _saving = false;
|
||||||
|
|
||||||
|
|
@ -42,6 +43,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
Future<void> _load() async {
|
Future<void> _load() async {
|
||||||
_name.text = await widget.profileStore.name();
|
_name.text = await widget.profileStore.name();
|
||||||
_about.text = await widget.profileStore.about();
|
_about.text = await widget.profileStore.about();
|
||||||
|
_g1.text = await widget.profileStore.g1();
|
||||||
if (mounted) setState(() => _loading = false);
|
if (mounted) setState(() => _loading = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +59,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
final t = context.t;
|
final t = context.t;
|
||||||
setState(() => _saving = true);
|
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.
|
// Best-effort publish to the network so peers see the name.
|
||||||
final relays = await widget.settings.relayUrls();
|
final relays = await widget.settings.relayUrls();
|
||||||
|
|
@ -67,6 +70,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
await session.profile.publish(
|
await session.profile.publish(
|
||||||
name: _name.text.trim(),
|
name: _name.text.trim(),
|
||||||
about: _about.text.trim(),
|
about: _about.text.trim(),
|
||||||
|
g1: _g1.text.trim(),
|
||||||
);
|
);
|
||||||
await session.close();
|
await session.close();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|
@ -82,6 +86,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_name.dispose();
|
_name.dispose();
|
||||||
_about.dispose();
|
_about.dispose();
|
||||||
|
_g1.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,6 +121,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
helperText: t.profile.aboutHint,
|
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),
|
const SizedBox(height: 24),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
key: const Key('profile.save'),
|
key: const Key('profile.save'),
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,13 @@ class NostrProfileTransport implements ProfileTransport {
|
||||||
String name = '',
|
String name = '',
|
||||||
String about = '',
|
String about = '',
|
||||||
String picture = '',
|
String picture = '',
|
||||||
|
String g1 = '',
|
||||||
}) async {
|
}) async {
|
||||||
final content = jsonEncode({
|
final content = jsonEncode({
|
||||||
if (name.isNotEmpty) 'name': name,
|
if (name.isNotEmpty) 'name': name,
|
||||||
if (about.isNotEmpty) 'about': about,
|
if (about.isNotEmpty) 'about': about,
|
||||||
if (picture.isNotEmpty) 'picture': picture,
|
if (picture.isNotEmpty) 'picture': picture,
|
||||||
|
if (g1.isNotEmpty) 'g1': g1,
|
||||||
});
|
});
|
||||||
final event = Event.from(
|
final event = Event.from(
|
||||||
kind: kindMetadata,
|
kind: kindMetadata,
|
||||||
|
|
@ -50,6 +52,7 @@ class NostrProfileTransport implements ProfileTransport {
|
||||||
name: (map['name'] as String?) ?? '',
|
name: (map['name'] as String?) ?? '',
|
||||||
about: (map['about'] as String?) ?? '',
|
about: (map['about'] as String?) ?? '',
|
||||||
picture: (map['picture'] as String?) ?? '',
|
picture: (map['picture'] as String?) ?? '',
|
||||||
|
g1: (map['g1'] as String?) ?? '',
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return UserProfile(pubkeyHex: pubkeyHex);
|
return UserProfile(pubkeyHex: pubkeyHex);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ class UserProfile {
|
||||||
this.name = '',
|
this.name = '',
|
||||||
this.about = '',
|
this.about = '',
|
||||||
this.picture = '',
|
this.picture = '',
|
||||||
|
this.g1 = '',
|
||||||
});
|
});
|
||||||
|
|
||||||
final String pubkeyHex;
|
final String pubkeyHex;
|
||||||
|
|
@ -13,7 +14,12 @@ class UserProfile {
|
||||||
final String about;
|
final String about;
|
||||||
final String picture;
|
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
|
/// 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.
|
/// sibling of the offer/message/trust transports over the same connection.
|
||||||
abstract interface class ProfileTransport {
|
abstract interface class ProfileTransport {
|
||||||
/// Publishes (replaces) this identity's profile.
|
/// Publishes (replaces) this identity's profile.
|
||||||
Future<void> publish({String name, String about, String picture});
|
Future<void> publish({String name, String about, String picture, String g1});
|
||||||
|
|
||||||
/// Fetches [pubkeyHex]'s latest profile, or null if they never published one.
|
/// Fetches [pubkeyHex]'s latest profile, or null if they never published one.
|
||||||
Future<UserProfile?> fetch(String pubkeyHex);
|
Future<UserProfile?> fetch(String pubkeyHex);
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,11 @@ void main() {
|
||||||
test('publish then fetch round-trips a profile (kind:0)', () async {
|
test('publish then fetch round-trips a profile (kind:0)', () async {
|
||||||
final alice = await idFor(1);
|
final alice = await idFor(1);
|
||||||
final aliceConn = await NostrConnection.connect(relay.url, identity: alice);
|
final aliceConn = await NostrConnection.connect(relay.url, identity: alice);
|
||||||
await NostrProfileTransport(aliceConn)
|
await NostrProfileTransport(aliceConn).publish(
|
||||||
.publish(name: 'Alicia', about: 'Guardo tomate rosa');
|
name: 'Alicia',
|
||||||
|
about: 'Guardo tomate rosa',
|
||||||
|
g1: 'G1abc123',
|
||||||
|
);
|
||||||
|
|
||||||
// Anyone else can fetch it.
|
// Anyone else can fetch it.
|
||||||
final bob = await idFor(2);
|
final bob = await idFor(2);
|
||||||
|
|
@ -28,6 +31,7 @@ void main() {
|
||||||
expect(fetched, isNotNull);
|
expect(fetched, isNotNull);
|
||||||
expect(fetched!.name, 'Alicia');
|
expect(fetched!.name, 'Alicia');
|
||||||
expect(fetched.about, 'Guardo tomate rosa');
|
expect(fetched.about, 'Guardo tomate rosa');
|
||||||
|
expect(fetched.g1, 'G1abc123');
|
||||||
|
|
||||||
await aliceConn.close();
|
await aliceConn.close();
|
||||||
await bobConn.close();
|
await bobConn.close();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue