Skip to content
Snippets Groups Projects
Commit 76a8a839 authored by vjrj's avatar vjrj
Browse files

Another hackathon

parent 0ef506fc
No related branches found
No related tags found
No related merge requests found
Showing
with 195 additions and 179 deletions
......@@ -43,6 +43,5 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
web
build.sh
linux/
\ No newline at end of file
linux/
......@@ -56,7 +56,6 @@
"data_load_error": "Error en carregar les dades",
"add_contact": "Afegeix el contacte",
"contact_added": "Contacte afegit",
"no_transactions": "Encara no tens cap transacció",
"qr-scanner-title": "Escaneja el QR d'algú",
"copy_contact_key": "Copia",
"nothing_found": "No s'ha trobat res",
......
......@@ -56,7 +56,7 @@
"data_load_error": "Error loading data",
"add_contact": "Add contact",
"contact_added": "Contact added",
"no_transactions": "You don't have any transaction yet",
"no_transactions": "This wallet has no balance. Start by offering your services in Ğ1 markets, for example, to receive your first income.",
"qr-scanner-title": "Scan the QR of someone",
"copy_contact_key": "Copy",
"nothing_found": "Nothing found",
......@@ -88,5 +88,6 @@
"please_confirm_sent_desc": "Please confirm that you wish to send {amount} Ğ1 to {to}",
"yes_sent": "YES, SEND IT",
"receive_g1": "Receive Ğ1",
"insufficient balance": "Oops, insufficient balance to pay this"
"insufficient balance": "Oops, insufficient balance to pay this",
"language_switch_title": "Select your language"
}
......@@ -56,7 +56,7 @@
"data_load_error": "Error al cargar los datos",
"add_contact": "Añadir contacto",
"contact_added": "Contacto añadido",
"no_transactions": "No tiene todavía ninguna transacción",
"no_transactions": "Este monedero no tiene saldo. Empieza por ejemplo a ofrecer tus servicios en mercados Ğ1 para recibir tus primeros ingresos",
"qr-scanner-title": "Escanea el QR de alguien",
"copy_contact_key": "Copiar",
"nothing_found": "No se ha encontrado nada",
......@@ -87,5 +87,6 @@
"please_confirm_sent_desc": "Por favor confirma que quieres enviar {amount} Ğ1 a {to}",
"yes_sent": "SÍ, ENVÍALO",
"receive_g1": "Recibe Junas",
"insufficient balance": "Upps, balance insuficiente para hacer este pago"
"insufficient balance": "Upps, balance insuficiente para hacer este pago",
"language_switch_title": "Selecciona tu idioma"
}
......@@ -19,7 +19,7 @@
"g1_form_pay_hint": "Entrez une description (facultatif)",
"code_card_title": "Dépôt de code",
"intro_1_title": "Bienvenue dans notre portefeuille Ğ1!",
"intro_1_description": "Avec ce portefeuille, vous pouvez facilement et en toute sécurité stocker, envoyer et recevoir de la monnaie Ğ1 (également connue sous le nom de 'Juin').",
"intro_1_description": "Avec ce portefeuille, vous pouvez facilement et en toute sécurité stocker, envoyer et recevoir de la monnaie Ğ1 (également connue sous le nom de 'June').",
"intro_2_title": "Une monnaie numérique libre créée par le peuple, pour le peuple",
"intro_2_description": "Ğ1 ne dépend d'aucun gouvernement ou entreprise et est écologique (car elle consomme peu d'énergie), transparente et équitable pour tous.",
"intro_3_title": "La monnaie Ğ1 fonctionne sur le réseau Duniter",
......@@ -63,4 +63,4 @@
"intro_some_pattern_to_export": "Motif pour importer/exporter votre portefeuille",
"confirm_pattern": "Confirmer le motif",
"draw_pattern": "Dessiner le motif"
}
\ No newline at end of file
}
......@@ -46,6 +46,7 @@
<string>en</string>
<string>es</string>
<string>fr</string>
<string>ca</string>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
......
......@@ -11,6 +11,7 @@ class AppState extends Equatable implements IsJsonSerializable<AppState> {
this.introViewed = false,
this.warningViewed = false,
this.expertMode = false,
this.locale = 'en',
});
factory AppState.fromJson(Map<String, dynamic> json) =>
......@@ -19,16 +20,19 @@ class AppState extends Equatable implements IsJsonSerializable<AppState> {
final bool introViewed;
final bool warningViewed;
final bool expertMode;
final String locale;
AppState copyWith({
bool? introViewed,
bool? warningViewed,
bool? expertMode,
String? locale,
}) {
return AppState(
introViewed: introViewed ?? this.introViewed,
warningViewed: warningViewed ?? this.warningViewed,
expertMode: expertMode ?? this.expertMode);
expertMode: expertMode ?? this.expertMode,
locale: locale ?? this.locale);
}
@override
......@@ -38,5 +42,6 @@ class AppState extends Equatable implements IsJsonSerializable<AppState> {
Map<String, dynamic> toJson() => _$AppStateToJson(this);
@override
List<Object?> get props => <Object>[introViewed, warningViewed, expertMode];
List<Object?> get props =>
<Object>[introViewed, warningViewed, expertMode, locale];
}
......@@ -10,10 +10,12 @@ AppState _$AppStateFromJson(Map<String, dynamic> json) => AppState(
introViewed: json['introViewed'] as bool? ?? false,
warningViewed: json['warningViewed'] as bool? ?? false,
expertMode: json['expertMode'] as bool? ?? false,
locale: json['locale'] as String? ?? 'en',
);
Map<String, dynamic> _$AppStateToJson(AppState instance) => <String, dynamic>{
'introViewed': instance.introViewed,
'warningViewed': instance.warningViewed,
'expertMode': instance.expertMode,
'locale': instance.locale,
};
......@@ -6,34 +6,6 @@ import 'node_list_state.dart';
class NodeListCubit extends HydratedCubit<NodeListState> {
NodeListCubit() : super(NodeListState());
void addDuniterNode(Node node) {
if (!_find(node)) {
// Does not exists, so add it
emit(state.copyWith(duniterNodes: <Node>[...state.duniterNodes, node]));
} else {
// it exists
updateDuniterNode(node);
}
}
bool _find(Node node) => state.duniterNodes.contains(node);
void insertDuniterNode(Node node) {
if (!_find(node)) {
emit(state.copyWith(duniterNodes: <Node>[node, ...state.duniterNodes]));
} else {
// it exists
updateDuniterNode(node);
}
}
void updateDuniterNode(Node updatedNode) {
final List<Node> updatedDuniterNodes = state.duniterNodes.map((Node n) {
return n.url == updatedNode.url ? updatedNode : n;
}).toList();
emit(state.copyWith(duniterNodes: updatedDuniterNodes));
}
void setDuniterNodes(List<Node> nodes) {
emit(state.copyWith(duniterNodes: nodes));
}
......@@ -42,9 +14,8 @@ class NodeListCubit extends HydratedCubit<NodeListState> {
emit(state.copyWith(cesiumPlusNodes: nodes));
}
void addCesiumPlusNode(Node node) {
emit(state
.copyWith(cesiumPlusNodes: <Node>[...state.cesiumPlusNodes, node]));
void setGvaNodes(List<Node> nodes) {
emit(state.copyWith(gvaNodes: nodes));
}
List<Node> get duniterNodes => state.duniterNodes;
......
......@@ -119,5 +119,6 @@ class NodeManagerObserver {
void update(NodeManager nodeManager) {
cubit.setDuniterNodes(nodeManager.duniterNodes);
cubit.setCesiumPlusNodes(nodeManager.cesiumPlusNodes);
cubit.setGvaNodes(nodeManager.gvaNodes);
}
}
......@@ -29,7 +29,7 @@ class PaymentCubit extends HydratedCubit<PaymentState> {
void selectKeyAmount(String publicKey, double amount) {
final PaymentState newState =
PaymentState(publicKey: publicKey, amount: amount);
PaymentState(publicKey: publicKey, amount: amount);
emit(newState);
}
......@@ -72,7 +72,7 @@ class PaymentCubit extends HydratedCubit<PaymentState> {
emit(newState);
}
void setDescription(String description) {
emit(state.copyWith(comment: description));
void setComment(String comment) {
emit(state.copyWith(comment: comment));
}
}
......@@ -30,7 +30,7 @@ class Transaction extends Equatable {
@JsonKey(fromJson: uIntFromList, toJson: uIntToList)
final Uint8List? toAvatar;
final String? toNick;
final int amount;
final double amount;
@JsonKey(fromJson: uIntFromList, toJson: uIntToList)
final Uint8List? fromAvatar;
final String? fromNick;
......@@ -65,7 +65,7 @@ class TransactionsAndBalanceState extends Equatable {
factory TransactionsAndBalanceState.fromJson(Map<String, dynamic> json) =>
_$TransactionsAndBalanceStateFromJson(json);
final List<Transaction> transactions;
final int balance;
final double balance;
final DateTime lastChecked;
Map<String, dynamic> toJson() => _$TransactionsAndBalanceStateToJson(this);
......
......@@ -11,7 +11,7 @@ abstract class _$TransactionCWProxy {
Transaction to(String to);
Transaction amount(int amount);
Transaction amount(double amount);
Transaction comment(String comment);
......@@ -34,7 +34,7 @@ abstract class _$TransactionCWProxy {
Transaction call({
String? from,
String? to,
int? amount,
double? amount,
String? comment,
DateTime? time,
Uint8List? toAvatar,
......@@ -57,7 +57,7 @@ class _$TransactionCWProxyImpl implements _$TransactionCWProxy {
Transaction to(String to) => this(to: to);
@override
Transaction amount(int amount) => this(amount: amount);
Transaction amount(double amount) => this(amount: amount);
@override
Transaction comment(String comment) => this(comment: comment);
......@@ -108,7 +108,7 @@ class _$TransactionCWProxyImpl implements _$TransactionCWProxy {
amount: amount == const $CopyWithPlaceholder() || amount == null
? _value.amount
// ignore: cast_nullable_to_non_nullable
: amount as int,
: amount as double,
comment: comment == const $CopyWithPlaceholder() || comment == null
? _value.comment
// ignore: cast_nullable_to_non_nullable
......@@ -146,7 +146,7 @@ extension $TransactionCopyWith on Transaction {
abstract class _$TransactionsAndBalanceStateCWProxy {
TransactionsAndBalanceState transactions(List<Transaction> transactions);
TransactionsAndBalanceState balance(int balance);
TransactionsAndBalanceState balance(double balance);
TransactionsAndBalanceState lastChecked(DateTime lastChecked);
......@@ -158,7 +158,7 @@ abstract class _$TransactionsAndBalanceStateCWProxy {
/// ````
TransactionsAndBalanceState call({
List<Transaction>? transactions,
int? balance,
double? balance,
DateTime? lastChecked,
});
}
......@@ -175,7 +175,7 @@ class _$TransactionsAndBalanceStateCWProxyImpl
this(transactions: transactions);
@override
TransactionsAndBalanceState balance(int balance) => this(balance: balance);
TransactionsAndBalanceState balance(double balance) => this(balance: balance);
@override
TransactionsAndBalanceState lastChecked(DateTime lastChecked) =>
......@@ -203,7 +203,7 @@ class _$TransactionsAndBalanceStateCWProxyImpl
balance: balance == const $CopyWithPlaceholder() || balance == null
? _value.balance
// ignore: cast_nullable_to_non_nullable
: balance as int,
: balance as double,
lastChecked:
lastChecked == const $CopyWithPlaceholder() || lastChecked == null
? _value.lastChecked
......@@ -227,7 +227,7 @@ extension $TransactionsAndBalanceStateCopyWith on TransactionsAndBalanceState {
Transaction _$TransactionFromJson(Map<String, dynamic> json) => Transaction(
from: json['from'] as String,
to: json['to'] as String,
amount: json['amount'] as int,
amount: (json['amount'] as num).toDouble(),
comment: json['comment'] as String,
time: DateTime.parse(json['time'] as String),
toAvatar: uIntFromList(json['toAvatar'] as List<int>),
......@@ -255,7 +255,7 @@ TransactionsAndBalanceState _$TransactionsAndBalanceStateFromJson(
transactions: (json['transactions'] as List<dynamic>)
.map((e) => Transaction.fromJson(e as Map<String, dynamic>))
.toList(),
balance: json['balance'] as int,
balance: (json['balance'] as num).toDouble(),
lastChecked: DateTime.parse(json['lastChecked'] as String),
);
......
......@@ -19,28 +19,19 @@ class TransactionsCubit extends HydratedCubit<TransactionsAndBalanceState> {
final TransactionsAndBalanceState currentState = state;
final List<Transaction> newTransactions =
List<Transaction>.of(currentState.transactions)..add(transaction);
final int newBalance = currentState.balance + transaction.amount;
final double newBalance = currentState.balance + transaction.amount;
emit(currentState.copyWith(
transactions: newTransactions, balance: newBalance));
}
void updateTransactions(List<Transaction> newTransactions, int newBalance) {
void updateTransactions(
List<Transaction> newTransactions, double newBalance) {
emit(state.copyWith(transactions: newTransactions, balance: newBalance));
}
Future<void> fetchTransactions(NodeListCubit cubit) async {
// Future<TransactionsAndBalance> _loadTransactions(NodeListCubit cubit) async {
// carga de datos asíncrona
// ...
// disabled, as we have to change the nodes
// https://g1.asycn.io/gva
// https://duniter.pini.fr/gva
/* Gva(node: 'https://g1.asycn.io/gva')
.balance(SharedPreferencesHelper().getPubKey())
.then((double currentBal) => setState(() {
_balanceAmount = currentBal;
})); */
logger('Loading transactions');
logger('GVA balance: ${gvaBalance()}');
final String txData = txDebugging
? await getTxHistory('6DrGg8cftpkgffv4Y4Lse9HSjgc8coEQor3yvMPHAnVH')
: await getTxHistory(SharedPreferencesHelper().getPubKey());
......@@ -61,7 +52,7 @@ class TransactionsCubit extends HydratedCubit<TransactionsAndBalanceState> {
List<Transaction> get transactions => state.transactions;
int get balance => state.balance;
double get balance => state.balance;
DateTime get lastChecked => state.lastChecked;
}
......@@ -405,17 +405,11 @@ Future<http.Response> _requestWithRetry(
Future<String> pay(
{required String to, required double amount, String? comment}) async {
final List<Node> nodes = nodesWorkingList(NodeType.gva);
if (nodes.isNotEmpty) {
// reorder list to use others
nodes.shuffle();
final String output = getGvaNode();
if (Uri.tryParse(output) != null) {
final String node = output;
try {
// Reference of working proxy 'https://g1demo.comunes.net/proxy/g1v1.p2p.legal/gva/';
final String node =
'https://g1demo.comunes.net/proxy/${nodes.first.url.replaceFirst('https://', '').replaceFirst('http://', '')}/';
final Gva gva = Gva(node: node);
logger('Trying $node to get balance');
final CesiumWallet wallet = await SharedPreferencesHelper().getWallet();
logger('Current balance ${await gva.balance(wallet.pubkey)}');
......@@ -430,11 +424,45 @@ Future<String> pay(
logger('GVA replied with "$response"');
return response;
} catch (e, stacktrace) {
// move logger outside main
logger(e);
logger(stacktrace);
return "Oops! the payment failed. Something didn't work as expected";
}
}
return 'Sorry: I cannot find a working node to send the transaction';
return output;
}
String getGvaNode() {
final List<Node> nodes = nodesWorkingList(NodeType.gva);
if (nodes.isNotEmpty) {
// reorder list to use others
nodes.shuffle();
// Reference of working proxy 'https://g1demo.comunes.net/proxy/g1v1.p2p.legal/gva/';
final String node =
'https://g1demo.comunes.net/proxy/${nodes.first.url.replaceFirst('https://', '').replaceFirst('http://', '')}/';
return node;
} else {
return 'Sorry: I cannot find a working node to send the transaction';
}
}
Future<double> gvaBalance() async {
final String output = getGvaNode();
if (Uri.tryParse(output) != null) {
final String node = output;
try {
final Gva gva = Gva(node: node);
logger('Trying $node to get balance');
final CesiumWallet wallet = await SharedPreferencesHelper().getWallet();
final double balance = await gva.balance(wallet.pubkey);
logger('Current balance $balance');
return balance;
} catch (e, stacktrace) {
// move logger outside main
logger(e);
logger(stacktrace);
throw Exception('Oops! failed to obtain balance');
}
}
throw Exception('Sorry: I cannot find a working node to get your balance');
}
......@@ -10,14 +10,14 @@ TransactionsAndBalanceState transactionParser(String txData) {
final String pubKey = parsedTxData['pubkey'] as String;
final List<dynamic> listReceived = (parsedTxData['history']
as Map<String, dynamic>)['received'] as List<dynamic>;
int balance = 0;
double balance = 0;
final List<Transaction> tx = <Transaction>[];
for (final dynamic receivedRaw in listReceived) {
final Map<String, dynamic> received = receivedRaw as Map<String, dynamic>;
final int timestamp = received['blockstampTime'] as int;
final String comment = received['comment'] as String;
final List<dynamic> outputs = received['outputs'] as List<dynamic>;
final int amount = int.parse((outputs[0] as String).split(':')[0]);
final double amount = double.parse((outputs[0] as String).split(':')[0]);
final String? address1 = exp.firstMatch(outputs[0] as String)!.group(1);
final String? address2 = exp.firstMatch(outputs[1] as String)!.group(1);
if (pubKey == address1) {
......
......@@ -78,6 +78,7 @@ void main() async {
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('ca'),
],
fallbackLocale: const Locale('en'),
useFallbackTranslations: true,
......@@ -144,7 +145,8 @@ class _AppIntro extends State<AppIntro> {
createPageViewModel(
'intro_${i}_title',
'intro_${i}_description',
'assets/img/undraw_intro_$i.png'),
'assets/img/undraw_intro_$i.png',
context),
],
onDone: () => _onIntroEnd(buildContext),
showSkipButton: true,
......@@ -169,15 +171,26 @@ class _AppIntro extends State<AppIntro> {
}
PageViewModel createPageViewModel(
String title, String body, String imageAsset) {
String title, String body, String imageAsset, BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final TextStyle titleStyle = TextStyle(
color: colorScheme.primary,
fontWeight: FontWeight.bold,
fontSize: 24.0,
);
final TextStyle bodyStyle = TextStyle(
color: colorScheme.onSurface,
fontSize: 18.0,
);
return PageViewModel(
title: tr(title),
body: tr(body),
image: Image.asset(imageAsset),
decoration: const PageDecoration(
pageColor: Colors.white,
bodyTextStyle: TextStyle(fontSize: 18),
titleTextStyle: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
decoration: PageDecoration(
titleTextStyle: titleStyle,
bodyTextStyle: bodyStyle,
pageColor: colorScheme.background,
),
);
}
......
......@@ -82,7 +82,6 @@ class SharedPreferencesHelper {
String getPubKey() {
// At this point should exists
final String? pubKey = _prefs.getString(_pubKey);
logger('Public key: $pubKey');
return pubKey!;
}
......
......@@ -28,6 +28,38 @@ class FifthScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16),
physics: const BouncingScrollPhysics(),
children: <Widget>[
const SizedBox(height: 10),
DropdownButtonFormField<Locale>(
value: context.locale,
decoration: InputDecoration(
labelText: tr('language_switch_title'),
icon: const Icon(Icons.language),
border: const OutlineInputBorder(),
),
onChanged: (Locale? newLocale) {
context.setLocale(newLocale!);
},
items: const <DropdownMenuItem<Locale>>[
DropdownMenuItem<Locale>(
value: Locale('ca'),
child: Text('Català'),
),
DropdownMenuItem<Locale>(
value: Locale('en'),
child: Text('English'),
),
DropdownMenuItem<Locale>(
value: Locale('es'),
child: Text('Español'),
),
DropdownMenuItem<Locale>(
value: Locale('fr'),
child: Text('Français'),
),
// Add more DropdownMenuItem for more languages
],
),
const TextDivider(text: 'key_tools_title'),
GridView.count(
physics: const NeverScrollableScrollPhysics(),
......
......@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../data/models/app_cubit.dart';
import '../../data/models/app_state.dart';
import '../../data/models/payment_cubit.dart';
import '../../data/models/payment_state.dart';
import '../widgets/bottom_widget.dart';
......@@ -20,94 +21,66 @@ class FirstScreen extends StatefulWidget {
class _FirstScreenState extends State<FirstScreen> {
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!context.read<AppCubit>().isWarningViewed) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr('demo_desc')),
action: SnackBarAction(
label: 'OK',
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
context.read<AppCubit>().warningViewed();
},
),
),
);
}
});
return BlocBuilder<PaymentCubit, PaymentState>(
builder: (BuildContext context, PaymentState state) =>
Stack(children: <Widget>[
Scaffold(
appBar: AppBar(title: Text(tr('credit_card_title'))),
drawer: const CardDrawer(),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
//physics: const AlwaysScrollableScrollPhysics(),
//controller: _controller,
// shrinkWrap: true,
children: <Widget>[
CreditCard(),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Divider(
color: Theme.of(context)
.colorScheme
.onBackground
.withOpacity(.4),
Widget build(BuildContext context) =>
BlocBuilder<AppCubit, AppState>(
builder: (BuildContext context, AppState state) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!state.warningViewed) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(tr('demo_desc')),
action: SnackBarAction(
label: 'OK',
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
context.read<AppCubit>().warningViewed();
},
),
),
);
}
});
return BlocBuilder<PaymentCubit, PaymentState>(
builder: (BuildContext context, PaymentState state) =>
Stack(children: <Widget>[
Scaffold(
appBar: AppBar(title: Text(tr('credit_card_title'))),
drawer: const CardDrawer(),
body: ListView(
padding: const EdgeInsets.symmetric(
horizontal: 16),
//physics: const AlwaysScrollableScrollPhysics(),
//controller: _controller,
// shrinkWrap: true,
children: <Widget>[
CreditCard(),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24),
child: Divider(
color: Theme
.of(context)
.colorScheme
.onBackground
.withOpacity(.4),
),
),
const SizedBox(height: 10),
const PayContactSearchButton(),
const SizedBox(height: 10),
const PayForm(),
const BottomWidget()
])),
Visibility(
visible: state.status == PaymentStatus.sending,
child: Container(
color: Colors.black.withOpacity(0.5),
child: const Center(
child: CircularProgressIndicator(),
),
),
const SizedBox(height: 10),
const PayContactSearchButton(),
const SizedBox(height: 10),
const PayForm(),
const BottomWidget()
])),
Visibility(
visible: state.status == PaymentStatus.sending,
child: Container(
color: Colors.black.withOpacity(0.5),
child: const Center(
child: CircularProgressIndicator(),
),
),
)
]));
}
)
]));
});
}
/*
Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () => Scaffold.of(context).openDrawer(),
),
title: Text('My App'),
),
drawerEnableOpenDragGesture: true,
drawer: CustomDrawer(),
body: // ...
);
class Screen1 extends StatelessWidget {
const Screen1({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Screen 1')),
drawer: CustomDrawer(),
body: Center(
child: ElevatedButton(
child: Text('Open Drawer'),
onPressed: () {
Scaffold.of(context).openDrawer();
},
),
),
);
}
} */
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment