Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mobile-app/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

<data android:scheme="https" android:host="www.quantus.com" android:pathPrefix="/account" />
<data android:scheme="https" android:host="www.quantus.com" android:pathPrefix="/oauth" />
<data android:scheme="https" android:host="www.quantus.com" android:pathPrefix="/pay" />
</intent-filter>
</activity>

Expand Down
17 changes: 16 additions & 1 deletion mobile-app/lib/features/components/button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import 'package:flutter/material.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart';
import 'package:resonance_network_wallet/features/styles/app_text_theme.dart';
import 'package:resonance_network_wallet/v2/theme/app_colors.dart';

enum ButtonVariant { transparent, neutral, primary, success, danger, glass, glassOutline, dangerOutline }
enum ButtonVariant { transparent, neutral, primary, success, danger, glass, glassOutline, dangerOutline, accent }

class Button extends StatelessWidget {
final String label;
Expand Down Expand Up @@ -34,6 +35,8 @@ class Button extends StatelessWidget {
switch (variant) {
case ButtonVariant.neutral:
return context.themeColors.textSecondary;
case ButtonVariant.accent:
return Colors.black;
default:
return null;
}
Expand Down Expand Up @@ -169,6 +172,18 @@ class Button extends StatelessWidget {
);
break;

case ButtonVariant.accent:
buttonWidget = Container(
width: width,
padding: padding,
decoration: ShapeDecoration(
color: disabled ? disabledBtnColor : context.colors.accentGreen,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(buttonRadius)),
),
child: buttonContent,
);
break;

default:
buttonWidget = ClipRRect(
borderRadius: BorderRadius.circular(buttonRadius),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:resonance_network_wallet/features/styles/app_colors_theme.dart';
import 'package:resonance_network_wallet/features/styles/app_text_theme.dart';
import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart';
import 'package:resonance_network_wallet/shared/extensions/media_query_data_extension.dart';
import 'package:resonance_network_wallet/v2/screens/send/send_sheet.dart';

class SharedAddressActionSheet extends StatefulWidget {
final String address;
Expand Down Expand Up @@ -56,7 +57,8 @@ class _SharedAddressActionSheetState extends State<SharedAddressActionSheet> {
}

void _sendToAddress() {
Navigator.of(context).pushNamed('/send', arguments: widget.address);
Navigator.of(context).pop();
showSendSheetV2(context, address: widget.address);
}

void _closeSheet() {
Expand Down
10 changes: 10 additions & 0 deletions mobile-app/lib/providers/route_intent_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,13 @@ import 'package:quantus_sdk/quantus_sdk.dart';

final transactionIntentProvider = StateProvider<TransactionEvent?>((_) => null);
final sharedAccountIntentProvider = StateProvider<String?>((_) => null);

class PaymentIntent {
final String to;
final String amount;
final String? ref;

const PaymentIntent({required this.to, required this.amount, this.ref});
}

final paymentIntentProvider = StateProvider<PaymentIntent?>((_) => null);
16 changes: 16 additions & 0 deletions mobile-app/lib/providers/wallet_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,19 @@ class IsBalanceHiddenNotifier extends StateNotifier<bool> {
state = value;
}
}

final posModeProvider = StateNotifierProvider<PosModeNotifier, bool>((ref) {
final settingsService = ref.watch(settingsServiceProvider);
return PosModeNotifier(settingsService);
});

class PosModeNotifier extends StateNotifier<bool> {
final SettingsService _settingsService;

PosModeNotifier(this._settingsService) : super(_settingsService.isPosModeEnabled());

Future<void> setPosMode(bool value) async {
await _settingsService.setPosModeEnabled(value);
state = value;
}
}
13 changes: 13 additions & 0 deletions mobile-app/lib/services/deep_link_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ class DeepLinkService {
}
}

if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'pay') {
final to = uri.queryParameters['to'];
final amount = uri.queryParameters['amount'];
final ref = uri.queryParameters['ref'];

if (to != null && to.isNotEmpty && amount != null && amount.isNotEmpty) {
_ref.read(paymentIntentProvider.notifier).state = PaymentIntent(to: to, amount: amount, ref: ref);
navigatorKey.currentState?.pushNamed('/account');
} else {
print('Missing payment parameters');
}
}

if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'oauth') {
_ref.invalidate(accountAssociationsProvider);
}
Expand Down
25 changes: 25 additions & 0 deletions mobile-app/lib/services/pos_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class PosPaymentRequest {
final String paymentUrl;
final String refId;
final String amount;

const PosPaymentRequest({required this.paymentUrl, required this.refId, required this.amount});
}

class PosService {
String generateRefId() {
final now = DateTime.now().millisecondsSinceEpoch;
return now.toRadixString(36).toUpperCase();
}

String buildPaymentUrl({required String accountId, required String amount, required String refId}) {
final uri = Uri.https('www.quantus.com', '/pay', {'to': accountId, 'amount': amount, 'ref': refId});
return uri.toString();
}

PosPaymentRequest createPaymentRequest({required String accountId, required String amount}) {
final refId = generateRefId();
final url = buildPaymentUrl(accountId: accountId, amount: amount, refId: refId);
return PosPaymentRequest(paymentUrl: url, refId: refId, amount: amount);
}
}
41 changes: 38 additions & 3 deletions mobile-app/lib/v2/screens/home/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/features/components/account_gradient_image.dart';
import 'package:resonance_network_wallet/features/components/button.dart';
import 'package:resonance_network_wallet/features/components/shared_address_action_sheet.dart';
import 'package:resonance_network_wallet/features/components/skeleton.dart';
import 'package:resonance_network_wallet/v2/components/glass_button.dart';
import 'package:resonance_network_wallet/v2/components/glass_button.dart' hide ButtonVariant;
import 'package:resonance_network_wallet/v2/components/glass_icon_button.dart';
import 'package:resonance_network_wallet/v2/screens/accounts/accounts_sheet.dart';
import 'package:resonance_network_wallet/v2/screens/receive/receive_sheet.dart';
import 'package:resonance_network_wallet/v2/screens/send/send_sheet.dart';
import 'package:resonance_network_wallet/v2/screens/settings/settings_screen.dart';
import 'package:resonance_network_wallet/utils/feature_flags.dart';
import 'package:resonance_network_wallet/v2/screens/pos/pos_amount_screen.dart';
import 'package:resonance_network_wallet/v2/screens/swap/swap_screen.dart';
import 'package:resonance_network_wallet/providers/account_id_list_cache.dart';
import 'package:resonance_network_wallet/providers/account_providers.dart';
Expand Down Expand Up @@ -48,6 +50,15 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
}

void _processIntentIfAvailable() {
final payment = ref.read(paymentIntentProvider);
if (payment != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(paymentIntentProvider.notifier).state = null;
showSendSheetV2(context, address: payment.to, amount: payment.amount, isPayMode: true);
});
return;
}

final shared = ref.read(sharedAccountIntentProvider);
if (shared != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Expand All @@ -67,13 +78,14 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
_processIntentIfAvailable();

final isBalanceHidden = ref.watch(isBalanceHiddenProvider);
final isPosMode = ref.watch(posModeProvider);
final accountAsync = ref.watch(activeAccountProvider);
final balanceAsync = ref.watch(balanceProvider);
final txAsync = ref.watch(activeAccountTransactionsProvider);
final colors = context.colors;
final text = context.themeText;

return accountAsync.when(
Widget screen = accountAsync.when(
loading: () => ScaffoldBase(
child: Center(child: CircularProgressIndicator(color: colors.textPrimary)),
),
Expand All @@ -91,11 +103,34 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
slivers: [
_buildContent(active, balanceAsync, isBalanceHidden, colors, text),
ActivitySection(txAsync: txAsync, activeAccount: active.account, onRetry: _refresh),
const SizedBox(height: 58),
SizedBox(height: isPosMode ? 120 : 58),
],
);
},
);

if (!isPosMode) return screen;

return Stack(
children: [
screen,
Positioned(
left: 24,
right: 24,
bottom: MediaQuery.of(context).padding.bottom + 24,
child: _buildPosButton(colors, text),
),
],
);
}

Widget _buildPosButton(AppColorsV2 colors, AppTextTheme text) {
return Button(
label: 'New Charge',
variant: ButtonVariant.accent,
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const PosAmountScreen())),
textStyle: text.smallTitle?.copyWith(fontWeight: FontWeight.w700, fontSize: 20, decoration: TextDecoration.none),
);
}

Widget _buildContent(
Expand Down
143 changes: 143 additions & 0 deletions mobile-app/lib/v2/screens/pos/pos_amount_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/features/components/button.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base.dart';
import 'package:resonance_network_wallet/v2/components/v2_app_bar.dart';
import 'package:resonance_network_wallet/v2/screens/pos/pos_qr_screen.dart';
import 'package:resonance_network_wallet/v2/theme/app_colors.dart';
import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart';

class PosAmountScreen extends ConsumerStatefulWidget {
const PosAmountScreen({super.key});

@override
ConsumerState<PosAmountScreen> createState() => _PosAmountScreenState();
}

class _PosAmountScreenState extends ConsumerState<PosAmountScreen> {
String _input = '0';
final _fmt = NumberFormattingService();
final _decimalFilter = DecimalInputFilter();

void _onDigit(String digit) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use the number formatting that we use in the usual send input?

final oldText = _input == '0' && digit != '.' && digit != ',' ? '' : _input;
final newText = oldText + digit;

final oldValue = TextEditingValue(text: oldText);
final newValue = TextEditingValue(text: newText);

final formatted = _decimalFilter.formatEditUpdate(oldValue, newValue);

setState(() {
_input = formatted.text.isEmpty ? '0' : formatted.text;
});
}

void _onBackspace() {
setState(() {
if (_input.length <= 1) {
_input = '0';
} else {
_input = _input.substring(0, _input.length - 1);
}
});
}

void _onClear() => setState(() => _input = '0');

void _onCharge() {
final amount = _fmt.parseAmount(_input);
if (amount == null || amount <= BigInt.zero) return;
Navigator.push(context, MaterialPageRoute(builder: (_) => PosQrScreen(amount: _input)));
}

bool get _isValid {
final amount = _fmt.parseAmount(_input);
return amount != null && amount > BigInt.zero;
}

@override
Widget build(BuildContext context) {
final colors = context.colors;
final text = context.themeText;

return ScaffoldBase(
appBar: const V2AppBar(title: 'New Charge'),
child: Column(
children: [
Expanded(
child: Center(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'$_input ${AppConstants.tokenSymbol}',
style: text.extraLargeTitle?.copyWith(
color: colors.textPrimary,
fontSize: 56,
fontWeight: FontWeight.w600,
),
),
),
),
),
_buildKeypad(colors, text),
const SizedBox(height: 16),
_buildChargeButton(colors, text),
const SizedBox(height: 24),
],
),
);
}

Widget _buildKeypad(AppColorsV2 colors, AppTextTheme text) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really sure this is the best way to do this, because some locales use comma instead of dot to as decimal notation.

final decimalSeparator = NumberFormat().symbols.DECIMAL_SEP;
final keys = [
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
[decimalSeparator, '0', 'backspace'],
];

return Column(
children: keys.map((row) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: row.map((key) => _buildKey(key, colors, text)).toList(),
),
);
}).toList(),
);
}

Widget _buildKey(String key, AppColorsV2 colors, AppTextTheme text) {
return Expanded(
child: GestureDetector(
onTap: () => key == 'backspace' ? _onBackspace() : _onDigit(key),
onLongPress: key == 'backspace' ? _onClear : null,
behavior: HitTestBehavior.opaque,
child: Container(
height: 60,
alignment: Alignment.center,
child: key == 'backspace'
? Icon(Icons.backspace_outlined, color: colors.textPrimary, size: 28)
: Text(key, style: text.mediumTitle?.copyWith(color: colors.textPrimary, fontSize: 28)),
),
),
);
}

Widget _buildChargeButton(AppColorsV2 colors, AppTextTheme text) {
final disabled = !_isValid;
return Button(
label: _isValid ? 'Charge $_input ${AppConstants.tokenSymbol}' : 'Enter Amount',
variant: ButtonVariant.accent,
isDisabled: disabled,
onPressed: _onCharge,
textStyle: text.smallTitle?.copyWith(fontWeight: FontWeight.w700),
);
}
}
Loading
Loading