diff --git a/assets/translations/en.json b/assets/translations/en.json index e180cddc7ae4123bd451604f40287ceab3f60acf..3bcaf34b57c18ea3bb454d5f661049e01e38bcd8 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -190,5 +190,6 @@ "telegram_group": "Telegram Group", "payment_waiting_internet_title": "No Internet", "payment_waiting_internet_desc": "We'll retry your payment when internet is back", - "payment_waiting_internet_desc_beta": "Retry your payment when internet is back" + "payment_waiting_internet_desc_beta": "Retry your payment when internet is back", + "your_name_here": "Name this card" } diff --git a/assets/translations/es.json b/assets/translations/es.json index 8e772b94ec78d307306fec6601cf435af6632880..1b27cb327bdcfb0e2d4276f6ad6d1985acf26506 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -192,5 +192,6 @@ "telegram_group": "Grupo de Telegram", "payment_waiting_internet_title": "Sin Internet", "payment_waiting_internet_desc": "Reintentaremos el pago cuando vuelvas a tener conexión", - "payment_waiting_internet_desc_beta": "Reintenta el pago cuando vuelvas a tener conexión" + "payment_waiting_internet_desc_beta": "Reintenta el pago cuando vuelvas a tener conexión", + "your_name_here": "Da un nombre a esta tarjeta" } diff --git a/lib/data/models/cesium_card.dart b/lib/data/models/cesium_card.dart index cdce326d91269fcc614b60e5df86c6fea01b27f6..5702960c917326b5d6d767d9357f6101fcd250b8 100644 --- a/lib/data/models/cesium_card.dart +++ b/lib/data/models/cesium_card.dart @@ -1,3 +1,4 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:json_annotation/json_annotation.dart'; import 'credit_card_themes.dart'; @@ -6,6 +7,7 @@ import 'is_json_serializable.dart'; part 'cesium_card.g.dart'; @JsonSerializable() +@CopyWith() class CesiumCard implements IsJsonSerializable<CesiumCard> { CesiumCard( {required this.seed, diff --git a/lib/data/models/cesium_card.g.dart b/lib/data/models/cesium_card.g.dart index 8822d1bca96d8d869922b991eb8f6a16ab32500c..a4047c19202d615617df49832dc4bc4cec970f2f 100644 --- a/lib/data/models/cesium_card.g.dart +++ b/lib/data/models/cesium_card.g.dart @@ -2,6 +2,92 @@ part of 'cesium_card.dart'; +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$CesiumCardCWProxy { + CesiumCard seed(String seed); + + CesiumCard pubKey(String pubKey); + + CesiumCard name(String name); + + CesiumCard theme(CreditCardTheme theme); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `CesiumCard(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// CesiumCard(...).copyWith(id: 12, name: "My name") + /// ```` + CesiumCard call({ + String? seed, + String? pubKey, + String? name, + CreditCardTheme? theme, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfCesiumCard.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfCesiumCard.copyWith.fieldName(...)` +class _$CesiumCardCWProxyImpl implements _$CesiumCardCWProxy { + const _$CesiumCardCWProxyImpl(this._value); + + final CesiumCard _value; + + @override + CesiumCard seed(String seed) => this(seed: seed); + + @override + CesiumCard pubKey(String pubKey) => this(pubKey: pubKey); + + @override + CesiumCard name(String name) => this(name: name); + + @override + CesiumCard theme(CreditCardTheme theme) => this(theme: theme); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `CesiumCard(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// CesiumCard(...).copyWith(id: 12, name: "My name") + /// ```` + CesiumCard call({ + Object? seed = const $CopyWithPlaceholder(), + Object? pubKey = const $CopyWithPlaceholder(), + Object? name = const $CopyWithPlaceholder(), + Object? theme = const $CopyWithPlaceholder(), + }) { + return CesiumCard( + seed: seed == const $CopyWithPlaceholder() || seed == null + ? _value.seed + // ignore: cast_nullable_to_non_nullable + : seed as String, + pubKey: pubKey == const $CopyWithPlaceholder() || pubKey == null + ? _value.pubKey + // ignore: cast_nullable_to_non_nullable + : pubKey as String, + name: name == const $CopyWithPlaceholder() || name == null + ? _value.name + // ignore: cast_nullable_to_non_nullable + : name as String, + theme: theme == const $CopyWithPlaceholder() || theme == null + ? _value.theme + // ignore: cast_nullable_to_non_nullable + : theme as CreditCardTheme, + ); + } +} + +extension $CesiumCardCopyWith on CesiumCard { + /// Returns a callable class that can be used as follows: `instanceOfCesiumCard.copyWith(...)` or like so:`instanceOfCesiumCard.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$CesiumCardCWProxy get copyWith => _$CesiumCardCWProxyImpl(this); +} + // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** diff --git a/lib/g1/api.dart b/lib/g1/api.dart index 77dd154519e36481991120a9e141445ad2a69284..3531c79da0866b5fdd004f3356c495bee2878f15 100644 --- a/lib/g1/api.dart +++ b/lib/g1/api.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:crypto/crypto.dart'; import 'package:durt/durt.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -30,7 +31,7 @@ final String currency = currencyDotEnv.isEmpty ? 'g1' : currencyDotEnv; Future<String> getTxHistory(String publicKey) async { final Response response = - await requestWithRetry(NodeType.duniter, '/tx/history/$publicKey'); + await requestWithRetry(NodeType.duniter, '/tx/history/$publicKey'); if (response.statusCode == 200) { return response.body; } else { @@ -58,7 +59,7 @@ Future<Response> searchCPlusUser(String searchTerm) async { '/user/profile/_search?q=title:$searchTermLower OR issuer:$searchTerm OR title:$searchTermCapitalized OR title:$searchTerm'; final Response response = - await requestCPlusWithRetry(query, retryWith404: false); + await requestCPlusWithRetry(query, retryWith404: false); return response; } @@ -70,12 +71,12 @@ Future<Contact> getProfile(String pubKeyRaw, '/user/profile/$pubKey', retryWith404: false); final Map<String, dynamic> result = - const JsonDecoder().convert(cPlusResponse.body) as Map<String, dynamic>; + const JsonDecoder().convert(cPlusResponse.body) as Map<String, dynamic>; if (result['found'] == false) { return Contact(pubKey: pubKey); } final Map<String, dynamic> profile = - const JsonDecoder().convert(cPlusResponse.body) as Map<String, dynamic>; + const JsonDecoder().convert(cPlusResponse.body) as Map<String, dynamic>; final Contact c = await contactFromResultSearch(profile); if (!onlyCPlusProfile) { // This penalize the gva rate limit @@ -116,7 +117,7 @@ Future<List<Contact>> searchWot(String searchTermRaw) async { final List<Contact> contacts = <Contact>[]; if (response.statusCode == HttpStatus.ok) { final Map<String, dynamic> data = - json.decode(response.body) as Map<String, dynamic>; + json.decode(response.body) as Map<String, dynamic>; final List<dynamic> results = data['results'] as List<dynamic>; // logger('Returning wot results ${results.length}'); if (results.isNotEmpty) { @@ -143,11 +144,11 @@ Future<Contact> getWot(Contact contact) async { // Will be better to analyze the 404 response (to detect faulty node) if (response.statusCode == HttpStatus.ok) { final Map<String, dynamic> data = - json.decode(response.body) as Map<String, dynamic>; + json.decode(response.body) as Map<String, dynamic>; final List<dynamic> results = data['results'] as List<dynamic>; if (results.isNotEmpty) { final List<dynamic> uids = - (results[0] as Map<String, dynamic>)['uids'] as List<dynamic>; + (results[0] as Map<String, dynamic>)['uids'] as List<dynamic>; if (uids.isNotEmpty) { // ignore: avoid_dynamic_calls return contact.copyWith(nick: uids[0]!['uid'] as String); @@ -160,14 +161,14 @@ Future<Contact> getWot(Contact contact) async { @Deprecated('use getProfile') Future<String> _getDataImageFromKey(String publicKey) async { final Response response = - await requestCPlusWithRetry('/user/profile/$publicKey'); + await requestCPlusWithRetry('/user/profile/$publicKey'); if (response.statusCode == HttpStatus.ok) { final Map<String, dynamic> data = - json.decode(response.body) as Map<String, dynamic>; + json.decode(response.body) as Map<String, dynamic>; final Map<String, dynamic> source = data['_source'] as Map<String, dynamic>; if (source.containsKey('avatar')) { final Map<String, dynamic> avatarData = - source['avatar'] as Map<String, dynamic>; + source['avatar'] as Map<String, dynamic>; if (avatarData.containsKey('_content')) { final String content = avatarData['_content'] as String; return 'data:image/png;base64,$content'; @@ -262,16 +263,18 @@ Future<void> _fetchGvaNodes({bool force = false}) async { NodeManager().loading = false; } -int nodesWorking(NodeType type) => NodeManager() - .nodeList(type) - .where((Node n) => n.errors < NodeManager.maxNodeErrors) - .toList() - .length; +int nodesWorking(NodeType type) => + NodeManager() + .nodeList(type) + .where((Node n) => n.errors < NodeManager.maxNodeErrors) + .toList() + .length; -List<Node> nodesWorkingList(NodeType type) => NodeManager() - .nodeList(type) - .where((Node n) => n.errors < NodeManager.maxNodeErrors) - .toList(); +List<Node> nodesWorkingList(NodeType type) => + NodeManager() + .nodeList(type) + .where((Node n) => n.errors < NodeManager.maxNodeErrors) + .toList(); Future<List<Node>> _fetchDuniterNodesFromPeers(NodeType type) async { final List<Node> lNodes = <Node>[]; @@ -283,14 +286,14 @@ Future<List<Node>> _fetchDuniterNodesFromPeers(NodeType type) async { final Response response = await getPeers(); if (response.statusCode == 200) { final Map<String, dynamic> peerList = - jsonDecode(response.body) as Map<String, dynamic>; + jsonDecode(response.body) as Map<String, dynamic>; final List<dynamic> peers = (peerList['peers'] as List<dynamic>) .where((dynamic peer) => - (peer as Map<String, dynamic>)['currency'] == currency) + (peer as Map<String, dynamic>)['currency'] == currency) .where( (dynamic peer) => (peer as Map<String, dynamic>)['version'] == 10) .where((dynamic peer) => - (peer as Map<String, dynamic>)['status'] == 'UP') + (peer as Map<String, dynamic>)['status'] == 'UP') .toList(); // reorder peer list peers.shuffle(); @@ -298,7 +301,7 @@ Future<List<Node>> _fetchDuniterNodesFromPeers(NodeType type) async { final Map<String, dynamic> peer = peerR as Map<String, dynamic>; if (peer['endpoints'] != null) { final List<String> endpoints = - List<String>.from(peer['endpoints'] as List<dynamic>); + List<String>.from(peer['endpoints'] as List<dynamic>); for (int j = 0; j < endpoints.length; j++) { if (endpoints[j].startsWith(apyType)) { final String endpointUnParsed = endpoints[j]; @@ -310,7 +313,9 @@ Future<List<Node>> _fetchDuniterNodesFromPeers(NodeType type) async { final NodeCheck nodeCheck = await _pingNode(endpoint, type); final Duration latency = nodeCheck.latency; logger( - 'Evaluating node: $endpoint, latency ${latency.inMicroseconds} currentBlock: ${nodeCheck.currentBlock}'); + 'Evaluating node: $endpoint, latency ${latency + .inMicroseconds} currentBlock: ${nodeCheck + .currentBlock}'); final Node node = Node( url: endpoint, latency: latency.inMicroseconds, @@ -343,7 +348,8 @@ Future<List<Node>> _fetchDuniterNodesFromPeers(NodeType type) async { } } logger( - 'Fetched ${lNodes.length} ${type.name} nodes ordered by latency ${lNodes.isNotEmpty ? '(first: ${lNodes.first.url})' : '(zero nodes)'}'); + 'Fetched ${lNodes.length} ${type.name} nodes ordered by latency ${lNodes + .isNotEmpty ? '(first: ${lNodes.first.url})' : '(zero nodes)'}'); } catch (e, stacktrace) { await Sentry.captureException(e, stackTrace: stacktrace); logger('General error in fetch ${type.name} nodes: $e'); @@ -395,7 +401,8 @@ Future<List<Node>> _fetchNodes(NodeType type) async { } logger( - 'Fetched ${lNodes.length} ${type.name} nodes ordered by latency (first: ${lNodes.first.url})'); + 'Fetched ${lNodes.length} ${type + .name} nodes ordered by latency (first: ${lNodes.first.url})'); } catch (e, stacktrace) { await Sentry.captureException(e, stackTrace: stacktrace); logger('General error in fetch ${type.name}: $e'); @@ -412,7 +419,8 @@ Future<NodeCheck> _pingNode(String node, NodeType type) async { int currentBlock = 0; Duration latency; try { - final Stopwatch stopwatch = Stopwatch()..start(); + final Stopwatch stopwatch = Stopwatch() + ..start(); if (type == NodeType.duniter) { final Response response = await http .get(Uri.parse('$node/blockchain/current')) @@ -421,7 +429,7 @@ Future<NodeCheck> _pingNode(String node, NodeType type) async { latency = stopwatch.elapsed; if (response.statusCode == 200) { final Map<String, dynamic> json = - jsonDecode(response.body) as Map<String, dynamic>; + jsonDecode(response.body) as Map<String, dynamic>; currentBlock = json['number'] as int; } else { latency = wrongNodeDuration; @@ -430,7 +438,7 @@ Future<NodeCheck> _pingNode(String node, NodeType type) async { // see: http://g1.data.e-is.pro/network/peering await http .get(Uri.parse('$node/network/peering')) - // Decrease http timeout during ping + // Decrease http timeout during ping .timeout(timeout); stopwatch.stop(); latency = stopwatch.elapsed; @@ -446,7 +454,8 @@ Future<NodeCheck> _pingNode(String node, NodeType type) async { latency = balance >= 0 ? stopwatch.elapsed : wrongNodeDuration; } logger( - 'Ping tested in node $node ($type), latency ${latency.inMicroseconds}, current block $currentBlock'); + 'Ping tested in node $node ($type), latency ${latency + .inMicroseconds}, current block $currentBlock'); return NodeCheck(latency: latency, currentBlock: currentBlock); } catch (e) { // Handle exception when node is unavailable etc @@ -471,12 +480,21 @@ Future<http.Response> requestCPlusWithRetry(String path, } Future<http.Response> requestGvaWithRetry(String path, - {bool retryWith404 = true}) async { - return _requestWithRetry(NodeType.gva, path, true, retryWith404); -} - -Future<http.Response> _requestWithRetry( - NodeType type, String path, bool dontRecord, bool retryWith404) async { + {bool retryWith404 = true, + bool isGet = true, + Map<String, String>? headers, + Object? body, + Encoding? encoding}) async { + return _requestWithRetry(NodeType.gva, path, true, retryWith404, + isGet: isGet, headers: headers, body: body, encoding: encoding); +} + +Future<http.Response> _requestWithRetry(NodeType type, String path, + bool dontRecord, bool retryWith404, + {bool isGet = true, + Map<String, String>? headers, + Object? body, + Encoding? encoding}) async { final List<Node> nodes = NodeManager() .nodeList(type) .where((Node node) => node.errors <= NodeManager.maxNodeErrors) @@ -485,8 +503,8 @@ Future<http.Response> _requestWithRetry( nodes.addAll(type == NodeType.duniter ? defaultDuniterNodes : type == NodeType.cesiumPlus - ? defaultCesiumPlusNodes - : defaultGvaNodes); + ? defaultCesiumPlusNodes + : defaultGvaNodes); } for (final int timeout in <int>[10]) { // only one timeout for now @@ -495,10 +513,17 @@ Future<http.Response> _requestWithRetry( try { final Uri url = Uri.parse('${node.url}$path'); logger('Fetching $url (${type.name})'); - final int startTime = DateTime.now().millisecondsSinceEpoch; - final Response response = - await http.get(url).timeout(Duration(seconds: timeout)); - final int endTime = DateTime.now().millisecondsSinceEpoch; + final int startTime = DateTime + .now() + .millisecondsSinceEpoch; + final Response response = isGet + ? await http.get(url).timeout(Duration(seconds: timeout)) + : await http + .post(url, body: body, headers: headers, encoding: encoding) + .timeout(Duration(seconds: timeout)); + final int endTime = DateTime + .now() + .millisecondsSinceEpoch; final int newLatency = endTime - startTime; if (!kReleaseMode) { logger('response.statusCode: ${response.statusCode}'); @@ -543,11 +568,10 @@ Future<http.Response> _requestWithRetry( 'Cannot make the request to any of the ${nodes.length} nodes'); } -Future<PayResult> pay( - {required String to, - required double amount, - String? comment, - bool? useMempool}) async { +Future<PayResult> pay({required String to, + required double amount, + String? comment, + bool? useMempool}) async { try { final SelectedGvaNode selected = getGvaNode(); @@ -556,7 +580,8 @@ Future<PayResult> pay( final Gva gva = Gva(node: nodeUrl); final CesiumWallet wallet = await SharedPreferencesHelper().getWallet(); logger( - 'Trying $nodeUrl to send $amount to $to with comment ${comment ?? ''}'); + 'Trying $nodeUrl to send $amount to $to with comment ${comment ?? + ''}'); final String response = await gva.pay( recipient: extractPublicKey(to), @@ -616,7 +641,9 @@ class PayResult { String proxyfyNode(String nodeUrl) { final String url = inProduction && kIsWeb - ? '${window.location.protocol}//${window.location.hostname}/proxy/${nodeUrl.replaceFirst('https://', '').replaceFirst('http://', '')}/' + ? '${window.location.protocol}//${window.location + .hostname}/proxy/${nodeUrl.replaceFirst('https://', '').replaceFirst( + 'http://', '')}/' : nodeUrl; return url; } @@ -624,7 +651,7 @@ String proxyfyNode(String nodeUrl) { Future<Tuple2<Map<String, dynamic>?, Node>> gvaHistoryAndBalance( String pubKeyRaw, [int? pageSize, - String? cursor]) async { + String? cursor]) async { logger('Get tx history (page size: $pageSize: cursor $cursor)'); final String pubKey = extractPublicKey(pubKeyRaw); return gvaFunctionWrapper<Map<String, dynamic>>( @@ -641,8 +668,8 @@ Future<Tuple2<String?, Node>> gvaNick(String pubKey) async { pubKey, (Gva gva) => gva.getUsername(extractPublicKey(pubKey))); } -Future<Tuple2<T?, Node>> gvaFunctionWrapper<T>( - String pubKey, Future<T?> Function(Gva) specificFunction) async { +Future<Tuple2<T?, Node>> gvaFunctionWrapper<T>(String pubKey, + Future<T?> Function(Gva) specificFunction) async { final List<Node> nodes = _getBestGvaNodes(); for (int i = 0; i < nodes.length; i++) { final Node node = nodes[i]; @@ -673,8 +700,8 @@ List<Node> _getBestGvaNodes() { .toList(); final int maxCurrentBlock = fnodes.fold( 0, - (int max, Node node) => - node.currentBlock > max ? node.currentBlock : max); + (int max, Node node) => + node.currentBlock > max ? node.currentBlock : max); final List<Node> nodes = fnodes .where((Node node) => node.currentBlock == maxCurrentBlock) .toList(); @@ -699,3 +726,81 @@ void increaseNodeErrors(NodeType type, Node node) { logger('Increasing node errors of ${node.url} (${node.errors})'); NodeManager().updateNode(type, node.copyWith(errors: node.errors + 1)); } + +// http://doc.e-is.pro/cesium-plus-pod/REST_API.html#userprofile +// https://git.p2p.legal/axiom-team/jaklis/src/branch/master/lib/cesiumCommon.py +Future<void> createOrUpdateCesiumPlusUser(String name) async { + final CesiumWallet wallet = await SharedPreferencesHelper().getWallet(); + // Define the endpoints + final String pubKey = wallet.pubkey; + + // Make the HTTP GET request to check if the user exists + final http.Response getResponse = await _requestWithRetry( + NodeType.cesiumPlus, '/api/user/profile/$pubKey', false, false); + + // Prepare the user profile data + final Map<String, dynamic> userProfile = <String, dynamic>{ + 'version': 2, + 'issuer': pubKey, + 'title': name + userNameSuffix, + 'time': DateTime + .now() + .millisecondsSinceEpoch ~/ + 1000, // current time in seconds + }; + + final String userProfileJson = jsonEncode(userProfile); + final String signature = wallet.sign(userProfileJson); + userProfile['hash'] = calculateHash(userProfileJson); + userProfile['signature'] = signature; + + // Convert the user profile data into a JSON string again, now including hash and signature + final String userProfileJsonWithHashAndSignature = jsonEncode(userProfile); + + // Prepare the request headers + final Map<String, String> headers = <String, String>{ + 'Content-Type': 'application/json', + }; + + if (getResponse.statusCode == 200) { + // User exists, update the user profile + final http.Response updateResponse = await _requestWithRetry( + NodeType.cesiumPlus, '/api/user/profile/_update', false, false, + isGet: false, + headers: headers, + body: userProfileJsonWithHashAndSignature); + if (updateResponse.statusCode == 200) { + logger('User profile updated successfully.'); + } else { + logger( + 'Failed to update user profile. Status code: ${updateResponse + .statusCode}'); + } + } else if (getResponse.statusCode == 404) { + // User does not exist, create a new user profile + final http.Response createResponse = await _requestWithRetry( + NodeType.cesiumPlus, '/api/user/profile', false, false, + isGet: false, + headers: headers, + body: userProfileJsonWithHashAndSignature); + + if (createResponse.statusCode == 200) { + logger('User profile created successfully.'); + } else { + logger( + 'Failed to create user profile. Status code: ${createResponse + .statusCode}'); + } + } else { + logger( + 'Failed to check if user exists. Status code: ${getResponse + .statusCode}'); + } +} + + +String calculateHash(String input) { + final List<int> bytes = utf8.encode(input); // data being hashed + final Digest digest = sha256.convert(bytes); + return digest.toString().toUpperCase(); +} diff --git a/lib/shared_prefs.dart b/lib/shared_prefs.dart index 5bfa7ef0abf84078df4a1f6283676fee09fbe432..b988686f3736a79d209a03d109a477223dfeab78 100644 --- a/lib/shared_prefs.dart +++ b/lib/shared_prefs.dart @@ -113,6 +113,17 @@ class SharedPreferencesHelper { return '$pubKey:$checksum'; } + String getName({int index = 0}) { + final CesiumCard card = cesiumCards[index]; + return card.name; + } + + void setName({int index = 0, required String name}) { + final CesiumCard card = cesiumCards[index]; + cesiumCards[index] = card.copyWith(name: name); + saveCesiumCards(); + } + List<CesiumCard> get cards => cesiumCards; static const String _currentWalletIndexKey = 'current_wallet_index'; diff --git a/lib/ui/screens/sandbox.dart b/lib/ui/screens/sandbox.dart index cbe282a92350672e61cf85927c83ce57db2019bb..554e713d8e1b43e1d2dee42a262a46d71f747d31 100644 --- a/lib/ui/screens/sandbox.dart +++ b/lib/ui/screens/sandbox.dart @@ -1,5 +1,8 @@ import 'package:flutter_neumorphic/flutter_neumorphic.dart'; +import '../../shared_prefs.dart'; +import '../widgets/first_screen/card_name_editable.dart'; + class Sandbox extends StatefulWidget { const Sandbox({super.key}); @@ -14,6 +17,6 @@ class _SandboxState extends State<Sandbox> { appBar: AppBar( title: const Text('Sandbox'), ), - body: const Placeholder()); + body: const CardNameEditable()); } } diff --git a/lib/ui/ui_helpers.dart b/lib/ui/ui_helpers.dart index 72ab13ed385a8d6a6121a9ade43b8f9ec12bcc9b..b91605020daa998e06766f9af8c8923f943683f3 100644 --- a/lib/ui/ui_helpers.dart +++ b/lib/ui/ui_helpers.dart @@ -516,3 +516,5 @@ void showQrDialog({ } bool get isIOS => !kIsWeb && Platform.isIOS; + +const String userNameSuffix = ' (Ğ1nkgo)'; diff --git a/lib/ui/widgets/first_screen/card_name_editable.dart b/lib/ui/widgets/first_screen/card_name_editable.dart new file mode 100644 index 0000000000000000000000000000000000000000..ae0d3fbbc94f92dfbc125cbbcb2cfdc551d09da2 --- /dev/null +++ b/lib/ui/widgets/first_screen/card_name_editable.dart @@ -0,0 +1,141 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import '../../../shared_prefs.dart'; +import '../../logger.dart'; +import '../../ui_helpers.dart'; +import 'card_text_style.dart'; + +class CardNameEditable extends StatefulWidget { + const CardNameEditable({super.key}); + + @override + State<CardNameEditable> createState() => _CardNameEditableState(); +} + +class _CardNameEditableState extends State<CardNameEditable> { + bool _isEditingText = false; + late TextEditingController _editingController; + late String currentText; + late String defValue; + late String name; + + @override + void initState() { + super.initState(); + defValue = tr('your_name_here'); + name = SharedPreferencesHelper().getName(); + currentText = _currentTextOrDef(); + _editingController = TextEditingController(text: currentText); + } + + String _currentTextOrDef() => + (name == tr('g1_wallet') || name.isEmpty) ? defValue : name; + + @override + void dispose() { + _editingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bool isSetted = currentText.isNotEmpty && currentText != defValue; + return _isEditingText + ? SizedBox( + width: 150.0, + child: SizedBox( + height: 40.0, + child: TextField( + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + vertical: 5.0, horizontal: 7.0), + filled: true, + fillColor: Colors.white, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.grey), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(width: 2.0), + ), + suffixIcon: IconButton( + icon: const Icon(Icons.check), + onPressed: () { + updateName(_editingController.text); + }, + ), + ), + cursorColor: Colors.black, + onSubmitted: (String newValue) { + updateName(newValue); + }, + // maxLength: 15, + autofocus: true, + controller: _editingController, + ))) + : Tooltip( + message: tr('your_name_here'), + child: InkWell( + child: RichText( + // softWrap: true, + maxLines: 2, + overflow: TextOverflow.ellipsis, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: <TextSpan>[ + if (currentText == defValue) + TextSpan( + text: currentText.toUpperCase(), + style: const TextStyle( + fontFamily: 'SourceCodePro', color: Colors.grey)), + if (isSetted) + TextSpan( + text: currentText, style: cardTextStyle(context, 15)), + /* TextSpan( + text: ' Lorem ipsum dolor sit amet, consectetur adipiscing elit', + style: cardTextStyle(context, 15), + ), */ + if (isSetted) + TextSpan( + text: userNameSuffix, + style: cardTextStyle(context, 10), + ), + ], + ), + ), + onTap: () { + setState(() { + try { + _editingController.selection = TextSelection( + baseOffset: 0, extentOffset: currentText.length); + } catch (e) { + logger(e); + } + _isEditingText = true; + }); + }, + )); + } + + void updateName(String newValue) { + setState(() { + if (newValue.isEmpty) { + // FIXME delete old name + SharedPreferencesHelper().setName(name: ''); + currentText = _currentTextOrDef(); + } else if (newValue == defValue) { + currentText = newValue; + } else { + try { + // await createOrUpdateCesiumPlusUser(newValue); + SharedPreferencesHelper().setName(name: newValue); + currentText = newValue; + } catch (e) { + logger(e); + // FIXME show message + } + } + _isEditingText = false; + }); + } +} diff --git a/lib/ui/widgets/first_screen/card_text_style.dart b/lib/ui/widgets/first_screen/card_text_style.dart index f345ca9967dcb8697de33f69df8ab96e3f91f5aa..a72c9db0b8fab1bab466eabee1e90cbd006faaf3 100644 --- a/lib/ui/widgets/first_screen/card_text_style.dart +++ b/lib/ui/widgets/first_screen/card_text_style.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; -TextStyle cardTextStyle(BuildContext context) { +TextStyle cardTextStyle(BuildContext context, + [double? fontSize, Color? color = Colors.white]) { return TextStyle( fontFamily: 'SourceCodePro', // decoration: TextDecoration.underline, - color: Colors.white, - fontSize: MediaQuery.of(context).size.width * 0.06, + color: color, + fontSize: fontSize ?? MediaQuery.of(context).size.width * 0.06, fontWeight: FontWeight.bold, shadows: <Shadow>[ Shadow( diff --git a/lib/ui/widgets/first_screen/credit_card.dart b/lib/ui/widgets/first_screen/credit_card.dart index 5b57b23f88754efd83a6b0aecbd3710bd05c4667..6326d0ca1c37a6a4b21465345fbbaae94d641c33 100644 --- a/lib/ui/widgets/first_screen/credit_card.dart +++ b/lib/ui/widgets/first_screen/credit_card.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/svg.dart'; import '../../../shared_prefs.dart'; import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; +import 'card_name_editable.dart'; import 'card_text_style.dart'; class CreditCard extends StatelessWidget { @@ -20,6 +21,7 @@ class CreditCard extends StatelessWidget { final double cardPadding = bigDevice ? 26.0 : 16.0; final String pubKey = SharedPreferencesHelper().getPubKey(); + return Card( elevation: 8.0, shape: RoundedRectangleBorder( @@ -76,18 +78,22 @@ class CreditCard extends StatelessWidget { Padding( padding: EdgeInsets.symmetric(horizontal: cardPadding), - child: GestureDetector( - onTap: () { - showQrDialog( - context: context, publicKey: pubKey); - }, - child: SvgPicture.asset( - width: MediaQuery.of(context).size.width < - smallScreenWidth - ? 25 - : 40, - 'assets/img/chip.svg', - ))), + child: Row(children: <Widget>[ + GestureDetector( + onTap: () { + showQrDialog( + context: context, publicKey: pubKey); + }, + child: SvgPicture.asset( + width: MediaQuery.of(context).size.width < + smallScreenWidth + ? 25 + : 40, + 'assets/img/chip.svg', + )), + const SizedBox(width: 10.0), + const Expanded(child: CardNameEditable()) + ])), const SizedBox(height: 6.0), Padding( padding: