diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml index 584f8b4a6b..176575f0b1 100644 --- a/.github/workflows/check_pr.yml +++ b/.github/workflows/check_pr.yml @@ -190,8 +190,10 @@ jobs: - name: Analyze run: | + pip3 install networkx graphviz pydot pydotplus melos exec -c 1 ${{ env.SCOPE }} -- make flutter_analyze melos exec -c 1 ${{ env.SCOPE }} -- make deps_graph_all + melos exec -c 1 ${{ env.SCOPE }} -- make deps_graph_features - name: Test run: melos exec -c 1 ${{ env.SCOPE }} --dir-exists="test" -- make flutter_test diff --git a/Makefile b/Makefile index 5a5b9ff3df..9e1033448e 100644 --- a/Makefile +++ b/Makefile @@ -94,8 +94,3 @@ ifndef VERSION endif dart run drift_dev schema dump lib/data/db/db.dart moor_schemas/moor_schema_v$(VERSION).json -deps_graph_all: - lakos lib -i "{**.freezed.dart,**.g.dart,storybook/**,data/**,di.config.dart,di.dart,gen/**,l10n/gen/**,routing.dart,di.dart,generated_plugin_registrant.dart}" --metrics > deps.dot - -deps_graph_features: - lakos lib/features -i "{**.freezed.dart,**.g.dart,**/src/**}" --metrics > features.dot diff --git a/packages/espressocash_app/CHANGELOG.md b/packages/espressocash_app/CHANGELOG.md index 599bef3cd3..a1890449d0 100644 --- a/packages/espressocash_app/CHANGELOG.md +++ b/packages/espressocash_app/CHANGELOG.md @@ -1,3 +1,16 @@ +## 1.115.0 + + - **REFACTOR**: restructure qr_scanner. + - **REFACTOR**: break outgoing_direct_payments -> wallet_flow. + - **REFACTOR**: break fees -> ramp. + - **REFACTOR**: break balances -> conversion_rates. + - **REFACTOR**: break conversion_rates -> wallet_flow. + - **REFACTOR**: break activities -> authenticated. + - **REFACTOR**: restructure accounts. + - **REFACTOR**: remove nested dependency (#1401). + - **FEAT**: update pay verification interval on opened request (#1410). + - **FEAT**: show add cash notice on zero balance (#1388). + ## 1.114.0 - **REFACTOR**: remove go_router (#1399). diff --git a/packages/espressocash_app/Makefile b/packages/espressocash_app/Makefile index db865483b3..a3b7aacb2b 100644 --- a/packages/espressocash_app/Makefile +++ b/packages/espressocash_app/Makefile @@ -21,3 +21,9 @@ flutter_test: deps_cycles: python3 ./tool/cycles.py deps.dot --only-shortest + +deps_graph_all: + lakos lib -i "{**.freezed.dart,**.g.dart,storybook/**,data/**,di.config.dart,di.dart,gen/**,l10n/gen/**,routing.dart,di.dart,generated_plugin_registrant.dart}" --metrics > deps.dot + +deps_graph_features: + dcm as lib/features --exclude="" --modules="/features/" > features.dot && python3 ./tool/cycles.py features.dot --only-shortest && echo "No cycles" diff --git a/packages/espressocash_app/ios/Podfile.lock b/packages/espressocash_app/ios/Podfile.lock index 7464881366..836bbe66d2 100644 --- a/packages/espressocash_app/ios/Podfile.lock +++ b/packages/espressocash_app/ios/Podfile.lock @@ -145,7 +145,7 @@ PODS: - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) - MLImage (= 1.0.0-beta5) - MLKitCommon (~> 11.0) - - mobile_scanner (5.0.0): + - mobile_scanner (5.0.2): - Flutter - GoogleMLKit/BarcodeScanning (~> 6.0.0) - nanopb (2.30909.1): @@ -168,13 +168,11 @@ PODS: - Ramp - rive_common (0.0.1): - Flutter - - Sentry/HybridSDK (8.21.0): - - SentryPrivate (= 8.21.0) - - sentry_flutter (8.0.0): + - Sentry/HybridSDK (8.25.0) + - sentry_flutter (8.1.0): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.21.0) - - SentryPrivate (8.21.0) + - Sentry/HybridSDK (= 8.25.0) - share (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -257,7 +255,6 @@ SPEC REPOS: - OrderedSet - PromisesObjC - Sentry - - SentryPrivate - sqlite3 EXTERNAL SOURCES: @@ -351,7 +348,7 @@ SPEC CHECKSUMS: MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1 MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1 - mobile_scanner: 43a31484164e3a7f816c15765d5bf88bc35ff942 + mobile_scanner: cfc76f77dca7e074fc9ca5993e3e7c35901c8b34 nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c @@ -361,9 +358,8 @@ SPEC CHECKSUMS: Ramp: 3f843fb75cac12ad40842afa3226bae36dc93521 ramp_flutter: aac85dee8bc93b5f7563c909ba15a37727fe3fe8 rive_common: 8630be7f6385198f9d31a5bc8cd7f6ccccedf5a1 - Sentry: ebc12276bd17613a114ab359074096b6b3725203 - sentry_flutter: 4ce59806771a82cc2d2a4657b320c9801530db62 - SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe + Sentry: cd86fc55628f5b7c572cabe66cc8f95a9d2f165a + sentry_flutter: ca7760fc008dc3bc2981730dc0c1d2f892178370 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec diff --git a/packages/espressocash_app/lib/features/authenticated/screens/main_screen.dart b/packages/espressocash_app/lib/features/authenticated/screens/main_screen.dart index 5dc99ec8af..d91574a233 100644 --- a/packages/espressocash_app/lib/features/authenticated/screens/main_screen.dart +++ b/packages/espressocash_app/lib/features/authenticated/screens/main_screen.dart @@ -3,22 +3,21 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../../di.dart'; -import '../../../gen/assets.gen.dart'; import '../../../ui/colors.dart'; -import '../../../ui/icon_button.dart'; import '../../../ui/navigation_bar/navigation_bar.dart'; import '../../../ui/page_fade_wrapper.dart'; import '../../../ui/theme.dart'; +import '../../../ui/value_stream_builder.dart'; import '../../activities/services/tx_updater.dart'; import '../../activities/widgets/recent_activity.dart'; -import '../../currency/models/currency.dart'; -import '../../profile/screens/profile_screen.dart'; -import '../../wallet_flow/widgets/launch_qr_scanner_flow.dart'; +import '../../conversion_rates/services/watch_user_fiat_balance.dart'; +import '../widgets/home_add_cash.dart'; +import '../widgets/home_app_bar.dart'; import '../widgets/home_carousel.dart'; import '../widgets/investment_header.dart'; import '../widgets/refresh_balance_wrapper.dart'; -class MainScreen extends StatefulWidget { +class MainScreen extends StatelessWidget { const MainScreen({ super.key, required this.onSendMoneyPressed, @@ -29,96 +28,83 @@ class MainScreen extends StatefulWidget { final VoidCallback onTransactionsPressed; @override - State createState() => _MainScreenState(); + Widget build(BuildContext context) => CpTheme.dark( + child: ValueStreamBuilder( + create: () => + sl().call().map((it) => it.isZero), + builder: (context, isZeroAmount) => isZeroAmount + ? const HomeAddCashContent() + : _MainContent( + onSendMoneyPressed: onSendMoneyPressed, + onTransactionsPressed: onTransactionsPressed, + ), + ), + ); } -class _MainScreenState extends State { - Future _handleScanPressed() => - context.launchQrScannerFlow(cryptoCurrency: Currency.usdc); +class _MainContent extends StatelessWidget { + const _MainContent({ + required this.onSendMoneyPressed, + required this.onTransactionsPressed, + }); + + final VoidCallback onSendMoneyPressed; + final VoidCallback onTransactionsPressed; @override - Widget build(BuildContext context) => CpTheme.dark( - child: PageFadeWrapper( - child: Container( - padding: const EdgeInsets.only(bottom: cpNavigationBarheight), - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - CpColors.darkSplashBackgroundColor, - CpColors.dashboardBackgroundColor, - ], - stops: [0.49, 0.51], - ), + Widget build(BuildContext context) => PageFadeWrapper( + child: Container( + padding: const EdgeInsets.only(bottom: cpNavigationBarheight), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + CpColors.darkSplashBackgroundColor, + CpColors.dashboardBackgroundColor, + ], + stops: [0.49, 0.51], ), - child: RefreshBalancesWrapper( - builder: (context, onRefresh) => RefreshIndicator( - displacement: 80, - onRefresh: () => Future.wait([ - onRefresh(), - sl().call(), - ]), - color: CpColors.primaryColor, - backgroundColor: Colors.white, - child: CustomScrollView( - slivers: [ - SliverAppBar( - leading: Center( - child: CpIconButton( - icon: Assets.icons.qrScanner.svg(), - variant: CpIconButtonVariant.black, - onPressed: _handleScanPressed, - ), - ), - shape: const Border(), - title: Center( - child: Assets.images.logo.image(height: 32), - ), - pinned: true, - snap: false, - floating: false, - elevation: 0, - backgroundColor: CpColors.darkBackground, - actions: [ - CpIconButton( - icon: Assets.icons.settingsButtonIcon - .svg(color: Colors.white), - variant: CpIconButtonVariant.black, - onPressed: () => ProfileScreen.push(context), - ), - const SizedBox(width: 12), - ], - toolbarHeight: kToolbarHeight + 12, - ), - SliverToBoxAdapter( - child: InvestmentHeader( - onSendMoneyPressed: widget.onSendMoneyPressed, - ), + ), + child: RefreshBalancesWrapper( + builder: (context, onRefresh) => RefreshIndicator( + displacement: 80, + onRefresh: () => Future.wait([ + onRefresh(), + sl().call(), + ]), + color: CpColors.primaryColor, + backgroundColor: Colors.white, + child: CustomScrollView( + slivers: [ + const HomeAppBar(), + SliverToBoxAdapter( + child: InvestmentHeader( + onSendMoneyPressed: onSendMoneyPressed, ), - SliverToBoxAdapter( - child: HomeCarouselWidget( - onSendMoneyPressed: widget.onSendMoneyPressed, - ), + ), + SliverToBoxAdapter( + child: HomeCarouselWidget( + onSendMoneyPressed: onSendMoneyPressed, ), - SliverToBoxAdapter( - child: RecentActivityWidget( - onSendMoneyPressed: widget.onSendMoneyPressed, - onTransactionsPressed: widget.onTransactionsPressed, - ), + ), + SliverToBoxAdapter( + child: RecentActivityWidget( + onSendMoneyPressed: onSendMoneyPressed, + onTransactionsPressed: onTransactionsPressed, ), - SliverToBoxAdapter( - child: SizedBox( - height: max( - 0, - MediaQuery.paddingOf(context).bottom - - cpNavigationBarheight + - 16, - ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: max( + 0, + MediaQuery.paddingOf(context).bottom - + cpNavigationBarheight + + 16, ), ), - ], - ), + ), + ], ), ), ), diff --git a/packages/espressocash_app/lib/features/authenticated/widgets/balance_amount.dart b/packages/espressocash_app/lib/features/authenticated/widgets/balance_amount.dart new file mode 100644 index 0000000000..197c173f29 --- /dev/null +++ b/packages/espressocash_app/lib/features/authenticated/widgets/balance_amount.dart @@ -0,0 +1,46 @@ +import 'package:dfunc/dfunc.dart'; +import 'package:flutter/material.dart'; + +import '../../../di.dart'; +import '../../../l10n/device_locale.dart'; +import '../../../ui/value_stream_builder.dart'; +import '../../conversion_rates/services/watch_user_fiat_balance.dart'; +import '../../conversion_rates/widgets/extensions.dart'; +import '../../currency/models/amount.dart'; +import '../../tokens/token.dart'; +import '../../tokens/widgets/token_icon.dart'; + +class BalanceAmount extends StatelessWidget { + const BalanceAmount({super.key}); + + @override + Widget build(BuildContext context) => ValueStreamBuilder( + create: () => sl().call(), + builder: (context, amount) { + final formattedAmount = amount.format( + DeviceLocale.localeOf(context), + roundInteger: amount.isZero, + ); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FittedBox( + child: Text( + formattedAmount, + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: -1, + ), + ), + ).let((it) => amount.isZero ? it : Flexible(child: it)), + const SizedBox(width: 8), + const TokenIcon(token: Token.usdc, size: 30), + ], + ); + }, + ); +} diff --git a/packages/espressocash_app/lib/features/authenticated/widgets/home_add_cash.dart b/packages/espressocash_app/lib/features/authenticated/widgets/home_add_cash.dart new file mode 100644 index 0000000000..3ebdb84024 --- /dev/null +++ b/packages/espressocash_app/lib/features/authenticated/widgets/home_add_cash.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../../../di.dart'; +import '../../../gen/assets.gen.dart'; +import '../../../l10n/l10n.dart'; +import '../../../ui/arrow.dart'; +import '../../../ui/bullet_item.dart'; +import '../../../ui/button.dart'; +import '../../../ui/colors.dart'; +import '../../accounts/models/account.dart'; +import '../../activities/services/tx_updater.dart'; +import '../../ramp/models/ramp_type.dart'; +import '../../ramp/widgets/ramp_buttons.dart'; +import 'balance_amount.dart'; +import 'home_app_bar.dart'; +import 'refresh_balance_wrapper.dart'; + +class HomeAddCashContent extends StatelessWidget { + const HomeAddCashContent({super.key}); + + @override + Widget build(BuildContext context) => ColoredBox( + color: CpColors.yellowSplashBackgroundColor, + child: RefreshBalancesWrapper( + builder: (context, onRefresh) => RefreshIndicator( + displacement: 80, + onRefresh: () => Future.wait([ + onRefresh(), + sl().call(), + ]), + color: CpColors.primaryColor, + backgroundColor: Colors.white, + child: const Stack( + children: [ + _Background(), + CustomScrollView( + physics: NeverScrollableScrollPhysics(), + slivers: [ + HomeAppBar(backgroundColor: Colors.transparent), + SliverFillRemaining( + child: IntrinsicHeight( + child: Column( + children: [ + Spacer(flex: 3), + _NoticeContent(), + Spacer(flex: 2), + _BottomBalance(), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); +} + +class _NoticeContent extends StatelessWidget { + const _NoticeContent(); + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.depositTitle.toUpperCase(), + style: TextStyle( + fontSize: 50.h, + fontWeight: FontWeight.w900, + letterSpacing: 0.25, + color: Colors.white, + ), + ), + SizedBox(height: 16.h), + Text( + context.l10n.addCashNoticeSubtitle.toUpperCase(), + style: TextStyle( + fontSize: 22.h, + fontWeight: FontWeight.w900, + letterSpacing: 0.25, + color: Colors.white, + ), + ), + SizedBox(height: 24.h), + CpBulletItemWidget( + child: Text.rich( + style: _bulletTextStyle, + textAlign: TextAlign.left, + TextSpan( + text: '${context.l10n.addCash_noInflation1}\n', + children: [ + TextSpan( + text: context.l10n.addCash_noInflation2, + ), + TextSpan( + text: context.l10n.addCash_noInflation3, + style: const TextStyle(color: Color(0xffFFDA66)), + ), + TextSpan( + text: context.l10n.addCash_noInflation4, + ), + ], + ), + ), + ), + const SizedBox(height: 8), + CpBulletItemWidget( + child: Text( + context.l10n.addCash_CashoutAnytime, + style: _bulletTextStyle, + ), + ), + const SizedBox(height: 8), + CpBulletItemWidget( + child: Text( + context.l10n.addCash_sendReceiveMoney, + style: _bulletTextStyle, + ), + ), + SizedBox(height: 28.h), + CpButton( + text: context.l10n.ramp_btnAddCash, + onPressed: () async { + final data = await context.ensureProfileData(RampType.onRamp); + if (context.mounted && data != null) { + context.launchOnRampFlow( + profile: data, + address: sl().address, + ); + } + }, + width: double.infinity, + size: CpButtonSize.big, + trailing: Padding( + padding: EdgeInsets.only(right: 8.h), + child: const Arrow(), + ), + ), + ], + ), + ); +} + +class _Background extends StatelessWidget { + const _Background(); + + @override + Widget build(BuildContext context) => SizedBox( + height: MediaQuery.sizeOf(context).height / 1.5, + child: Stack( + children: [ + Transform.rotate( + angle: 0.2, + child: Transform.translate( + offset: const Offset(-30, -25), + child: Assets.images.dollarBg.image( + fit: BoxFit.cover, + height: double.infinity, + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 400, + decoration: const BoxDecoration( + gradient: LinearGradient( + stops: [0.1, 1], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x00C8B57D), + CpColors.yellowSplashBackgroundColor, + ], + ), + ), + ), + ), + ], + ), + ); +} + +class _BottomBalance extends StatelessWidget { + const _BottomBalance(); + + @override + Widget build(BuildContext context) => DecoratedBox( + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(44), + topRight: Radius.circular(44), + ), + ), + color: CpColors.darkSplashBackgroundColor, + ), + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom, + ), + child: Center( + child: Padding( + padding: EdgeInsets.only( + top: 40.h, + bottom: 8.h, + ), + child: Column( + children: [ + Text( + context.l10n.cryptoCashBalance, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + letterSpacing: 0.23, + ), + ), + const BalanceAmount(), + ], + ), + ), + ), + ), + ); +} + +final _bulletTextStyle = TextStyle( + color: Colors.white, + fontSize: 18.h, + fontWeight: FontWeight.w700, + letterSpacing: 0.41, +); diff --git a/packages/espressocash_app/lib/features/authenticated/widgets/home_app_bar.dart b/packages/espressocash_app/lib/features/authenticated/widgets/home_app_bar.dart new file mode 100644 index 0000000000..e18aa1e8a9 --- /dev/null +++ b/packages/espressocash_app/lib/features/authenticated/widgets/home_app_bar.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import '../../../gen/assets.gen.dart'; +import '../../../ui/colors.dart'; +import '../../../ui/icon_button.dart'; +import '../../currency/models/currency.dart'; +import '../../profile/screens/profile_screen.dart'; +import '../../wallet_flow/widgets/launch_qr_scanner_flow.dart'; + +class HomeAppBar extends StatelessWidget { + const HomeAppBar({ + super.key, + this.backgroundColor = CpColors.darkBackground, + }); + + final Color backgroundColor; + + @override + Widget build(BuildContext context) => SliverAppBar( + leading: Center( + child: CpIconButton( + icon: Assets.icons.qrScanner.svg(), + variant: CpIconButtonVariant.black, + onPressed: () => + context.launchQrScannerFlow(cryptoCurrency: Currency.usdc), + ), + ), + shape: const Border(), + title: Center( + child: Assets.images.logo.image(height: 32), + ), + pinned: true, + snap: false, + floating: false, + elevation: 0, + backgroundColor: backgroundColor, + actions: [ + CpIconButton( + icon: Assets.icons.settingsButtonIcon.svg(color: Colors.white), + variant: CpIconButtonVariant.black, + onPressed: () => ProfileScreen.push(context), + ), + const SizedBox(width: 12), + ], + toolbarHeight: kToolbarHeight + 12, + ); +} diff --git a/packages/espressocash_app/lib/features/authenticated/widgets/investment_header.dart b/packages/espressocash_app/lib/features/authenticated/widgets/investment_header.dart index b4c20d2719..52072b326b 100644 --- a/packages/espressocash_app/lib/features/authenticated/widgets/investment_header.dart +++ b/packages/espressocash_app/lib/features/authenticated/widgets/investment_header.dart @@ -1,20 +1,14 @@ -import 'package:decimal/decimal.dart'; -import 'package:dfunc/dfunc.dart'; import 'package:flutter/material.dart'; import '../../../di.dart'; -import '../../../l10n/device_locale.dart'; import '../../../l10n/l10n.dart'; import '../../../ui/button.dart'; import '../../../ui/colors.dart'; import '../../../ui/info_icon.dart'; import '../../../ui/value_stream_builder.dart'; import '../../conversion_rates/services/watch_user_fiat_balance.dart'; -import '../../conversion_rates/widgets/extensions.dart'; -import '../../currency/models/amount.dart'; import '../../ramp/widgets/ramp_buttons.dart'; -import '../../tokens/token.dart'; -import '../../tokens/widgets/token_icon.dart'; +import 'balance_amount.dart'; class InvestmentHeader extends StatefulWidget { const InvestmentHeader({super.key, required this.onSendMoneyPressed}); @@ -44,7 +38,7 @@ class _InvestmentHeaderState extends State { children: [ _Headline(onInfo: _handleInfoPressed), const SizedBox(height: 4), - const _Amount(), + const BalanceAmount(), const SizedBox(height: 2), ], ), @@ -161,41 +155,6 @@ class _Info extends StatelessWidget { ); } -class _Amount extends StatelessWidget { - const _Amount(); - - @override - Widget build(BuildContext context) => ValueStreamBuilder( - create: () => sl().call(), - builder: (context, amount) { - final formattedAmount = amount.format( - DeviceLocale.localeOf(context), - roundInteger: amount.isZero, - ); - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FittedBox( - child: Text( - formattedAmount, - style: const TextStyle( - fontSize: 48, - fontWeight: FontWeight.bold, - color: Colors.white, - letterSpacing: -1, - ), - ), - ).let((it) => amount.isZero ? it : Flexible(child: it)), - const SizedBox(width: 8), - const TokenIcon(token: Token.usdc, size: 30), - ], - ); - }, - ); -} - class _Headline extends StatelessWidget { const _Headline({required this.onInfo}); @@ -233,10 +192,6 @@ class _Headline extends StatelessWidget { } } -extension on Amount { - bool get isZero => decimal == Decimal.zero; -} - class _HeaderSwitcher extends StatefulWidget { const _HeaderSwitcher({ required this.first, diff --git a/packages/espressocash_app/lib/features/currency/models/amount.dart b/packages/espressocash_app/lib/features/currency/models/amount.dart index f7410cfe1a..65c2a6e114 100644 --- a/packages/espressocash_app/lib/features/currency/models/amount.dart +++ b/packages/espressocash_app/lib/features/currency/models/amount.dart @@ -49,6 +49,8 @@ sealed class Amount with _$Amount { Decimal get decimal => Decimal.fromInt(value).shift(-currency.decimals); + bool get isZero => decimal == Decimal.zero; + Amount operator +(Amount other) { _ensureSameCurrency(other); diff --git a/packages/espressocash_app/lib/features/payment_request/screens/payment_request_screen.dart b/packages/espressocash_app/lib/features/payment_request/screens/payment_request_screen.dart index d6a5b6a7db..e7592e547a 100644 --- a/packages/espressocash_app/lib/features/payment_request/screens/payment_request_screen.dart +++ b/packages/espressocash_app/lib/features/payment_request/screens/payment_request_screen.dart @@ -1,9 +1,12 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import '../../../di.dart'; import '../../../ui/loader.dart'; import '../data/repository.dart'; import '../models/payment_request.dart'; +import '../services/payment_request_service.dart'; import '../widgets/request_success.dart'; import '../widgets/share_request.dart'; @@ -33,6 +36,21 @@ class _PaymentRequestScreenState extends State { void initState() { super.initState(); _stream = sl().watchById(widget.id); + + _watcher(); + } + + Future _watcher() async { + final request = await _stream.first; + + sl().initWatcher(request); + } + + @override + void dispose() { + sl().disposeWatcher(); + + super.dispose(); } @override diff --git a/packages/espressocash_app/lib/features/payment_request/services/payment_request_service.dart b/packages/espressocash_app/lib/features/payment_request/services/payment_request_service.dart index cd4966563c..0e42073c48 100644 --- a/packages/espressocash_app/lib/features/payment_request/services/payment_request_service.dart +++ b/packages/espressocash_app/lib/features/payment_request/services/payment_request_service.dart @@ -5,6 +5,7 @@ import 'package:dfunc/dfunc.dart'; import 'package:espressocash_api/espressocash_api.dart'; import 'package:flutter/foundation.dart'; +import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; import 'package:rxdart/rxdart.dart'; import 'package:solana/solana.dart'; @@ -20,7 +21,7 @@ import '../data/repository.dart'; import '../models/payment_request.dart'; @Singleton(scope: authScope) -class PaymentRequestService { +class PaymentRequestService implements Disposable { PaymentRequestService( this._repository, this._solanaClient, @@ -38,6 +39,8 @@ class PaymentRequestService { final Map> _subscriptions = {}; final Map _currentBackoffs = {}; + StreamSubscription? _watcher; + @PostConstruct(preResolve: true) Future init() async { final pendingPayments = await _repository.getAllPending(); @@ -48,14 +51,32 @@ class PaymentRequestService { } void _subscribe(PaymentRequest request) { - _waitForTx(request); + if (!request.state.isInitial) return; + + _subscriptions[request.id]?.cancel(); + _subscriptions[request.id] = _createSubscription(request); } - void _waitForTx(PaymentRequest request) { + void initWatcher(PaymentRequest request) { if (!request.state.isInitial) return; + _watcher?.cancel(); + _watcher = _createSubscription(request, interval: _focusedInterval); + } + + void disposeWatcher() { + _watcher?.cancel(); + } + + StreamSubscription _createSubscription( + PaymentRequest request, { + Duration interval = _backgroundInterval, + }) { final reference = request.payRequest.reference?.firstOrNull; - if (reference == null) return; + + if (reference == null) { + return const Stream.empty().listen(null); + } Stream solanaPayTransaction() => _solanaClient .findSolanaPayTransaction( @@ -65,10 +86,9 @@ class PaymentRequestService { .asStream() .whereType(); - _subscriptions[request.id] = - Stream.periodic(const Duration(seconds: 30)) - .flatMap((a) => solanaPayTransaction()) - .mergeWith([solanaPayTransaction()]).listen( + return Stream.periodic(interval) + .flatMap((a) => solanaPayTransaction()) + .mergeWith([solanaPayTransaction()]).listen( (id) { _verifyTx(id, request); }, @@ -80,8 +100,8 @@ class PaymentRequestService { _currentBackoffs[request.id] = _maxBackoff; } await Future.delayed(_currentBackoffs[request.id]!); - // ignore: avoid-recursive-calls, called in async callback - _waitForTx(request); + + _subscribe(request); }, ); } @@ -113,6 +133,7 @@ class PaymentRequestService { _refreshBalance(); await _subscriptions[request.id]?.cancel(); + await _watcher?.cancel(); } on Exception { _currentBackoffs[request.id] = (_currentBackoffs[request.id] ?? _minBackoff) * _backoffStep; @@ -169,9 +190,21 @@ class PaymentRequestService { return paymentRequest; } + Future cancel(String id) async { + await _repository.delete(id); + + await _subscriptions[id]?.cancel(); + } + Future unshortenLink(String shortLink) => _ecClient .unshortenLink(UnshortenLinkRequestDto(shortLink: shortLink)) .then((e) => Uri.parse(e.fullLink)); + + @override + Future onDispose() async { + await _watcher?.cancel(); + await Future.wait(_subscriptions.values.map((it) => it.cancel())); + } } Future _randomPublicKey([dynamic _]) async { @@ -184,6 +217,9 @@ const _backoffStep = 2; const _minBackoff = Duration(seconds: 2); const _maxBackoff = Duration(minutes: 1); +const _backgroundInterval = Duration(seconds: 30); +const _focusedInterval = Duration(seconds: 1); + extension on PaymentRequestState { bool get isInitial => this == PaymentRequestState.initial; } diff --git a/packages/espressocash_app/lib/features/payment_request/widgets/share_request.dart b/packages/espressocash_app/lib/features/payment_request/widgets/share_request.dart index 22ab4452df..298f64109f 100644 --- a/packages/espressocash_app/lib/features/payment_request/widgets/share_request.dart +++ b/packages/espressocash_app/lib/features/payment_request/widgets/share_request.dart @@ -12,8 +12,8 @@ import '../../../ui/text_button.dart'; import '../../../ui/theme.dart'; import '../../conversion_rates/widgets/extensions.dart'; import '../../tokens/token_list.dart'; -import '../data/repository.dart'; import '../models/payment_request.dart'; +import '../services/payment_request_service.dart'; class ShareRequestPayment extends StatelessWidget { const ShareRequestPayment({ @@ -94,7 +94,7 @@ class ShareRequestPayment extends StatelessWidget { message: context .l10n.paymentRequest_lblCancelConfirmationSubtitle, onConfirm: () { - sl().delete(request.id); + sl().cancel(request.id); Navigator.of(context).pop(); }, ), diff --git a/packages/espressocash_app/lib/l10n/intl_en.arb b/packages/espressocash_app/lib/l10n/intl_en.arb index 0e98b396cc..6ed6a0130f 100644 --- a/packages/espressocash_app/lib/l10n/intl_en.arb +++ b/packages/espressocash_app/lib/l10n/intl_en.arb @@ -790,6 +790,20 @@ "@outgoingDirectPayments_lblCancelConfirmationTitle": {}, "cancelTransferBtn": "Cancel transfer", "@cancelTransferBtn": {}, + "addCashNoticeSubtitle": "Deposit into your account", + "@addCashNoticeSubtitle": {}, + "addCash_noInflation1": "NO MORE INFLATION!", + "@addCash_noInflation1": {}, + "addCash_noInflation2": "With USDC ", + "@addCash_noInflation2": {}, + "addCash_noInflation3": "(digital US dollar)", + "@addCash_noInflation3": {}, + "addCash_noInflation4": ", your money doesn't lose value.", + "@addCash_noInflation4": {}, + "addCash_CashoutAnytime": "Cash-out anytime to your bank account at 1:1 value of US dollar.", + "@addCash_CashoutAnytime": {}, + "addCash_sendReceiveMoney": "Send & receive money in seconds.", + "@addCash_sendReceiveMoney": {}, "invoiceNumber": "Invoice {reference}", "@invoiceNumber": { "placeholders": { diff --git a/packages/espressocash_app/lib/storybook/stories/bullet_item.dart b/packages/espressocash_app/lib/storybook/stories/bullet_item.dart index 01a65fc79b..3df6eb1b3f 100644 --- a/packages/espressocash_app/lib/storybook/stories/bullet_item.dart +++ b/packages/espressocash_app/lib/storybook/stories/bullet_item.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; import '../../ui/bullet_item.dart'; @@ -5,6 +6,6 @@ import '../../ui/bullet_item.dart'; final cpBulletItem = Story( name: 'CpBulletItem', builder: (context) => const CpBulletItemWidget( - text: 'Espresso Cash Bullet Item Widget', + child: Text('Espresso Cash Bullet Item Widget'), ), ); diff --git a/packages/espressocash_app/lib/ui/bullet_item.dart b/packages/espressocash_app/lib/ui/bullet_item.dart index f40240d530..fc2146246c 100644 --- a/packages/espressocash_app/lib/ui/bullet_item.dart +++ b/packages/espressocash_app/lib/ui/bullet_item.dart @@ -1,47 +1,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; - import 'colors.dart'; class CpBulletItemWidget extends StatelessWidget { - const CpBulletItemWidget({ - super.key, - required this.text, - }); - - final String text; + const CpBulletItemWidget({super.key, required this.child}); + final Widget child; @override Widget build(BuildContext context) => Padding( - padding: EdgeInsets.only(left: 12.w, right: 39.w), + padding: EdgeInsets.only(left: 2.w, right: 16.w), child: IntrinsicHeight( child: Row( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ Container( width: 23.w, - margin: EdgeInsets.only(right: 19.w, top: 2.h, bottom: 2.h), + margin: EdgeInsets.only(right: 8.w, top: 4.h, bottom: 2.h), decoration: const ShapeDecoration( color: CpColors.yellowColor, - shape: StadiumBorder(), - ), - child: const SizedBox(), - ), - Expanded( - child: Text( - text, - textAlign: TextAlign.start, - textHeightBehavior: const TextHeightBehavior( - leadingDistribution: TextLeadingDistribution.even, - ), - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 20.sp, - ), + shape: CircleBorder(), ), + child: SizedBox(height: 14.h, width: 14.w), ), + Expanded(child: child), ], ), ), diff --git a/packages/espressocash_app/pubspec.lock b/packages/espressocash_app/pubspec.lock index 10f37cb9aa..d466b1b910 100644 --- a/packages/espressocash_app/pubspec.lock +++ b/packages/espressocash_app/pubspec.lock @@ -938,10 +938,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: fe9ee64ccb8d599a5dfb0e21cc6652232c610bcf667af4e79b9eb175cc30a7a5 + sha256: "33974eca2e87e8b4e3727f1b94fa3abcb25afe80b6bc2c4d449a0e150aedf720" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" image_picker_android: dependency: transitive description: @@ -1174,10 +1174,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: f34c83198d9381f6c100dfaec647c275630840cbcda5d6c5eb6ba264beb96be4 + sha256: "75bb4f875730f28bd5eb60fd76531ca762ba5d7c272d5b7d4fe55c20178f0a7b" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" mockito: dependency: "direct dev" description: @@ -1230,10 +1230,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "2c582551839386fa7ddbc7770658be7c0f87f388a4bff72066478f597c34d17f" + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "8.0.0" package_info_plus_platform_interface: dependency: transitive description: @@ -1526,18 +1526,18 @@ packages: dependency: transitive description: name: sentry - sha256: "961630a4dba41cebd692612421fd3805a991ebd8ef4a8d84a6c179e6b89eaf22" + sha256: "1d2952d40b99da0dc4bf3ba4797e3985dd60cc61a13d0a1d2c62b02f6528441a" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.1.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "705da7adfcd1fb23fc8720b6e222f5e03d53f1d020cd635586c319ee323366b4" + sha256: "848aaccfc75f1d35d5f7e5230770761f1b2a33e604f24498200566443da1470a" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.1.0" share: dependency: "direct main" description: diff --git a/packages/espressocash_app/pubspec.yaml b/packages/espressocash_app/pubspec.yaml index 374dc2636f..bf66a79871 100644 --- a/packages/espressocash_app/pubspec.yaml +++ b/packages/espressocash_app/pubspec.yaml @@ -1,7 +1,7 @@ name: espressocash_app description: Espresso Cash mobile wallet for Solana publish_to: "none" -version: 1.114.0 +version: 1.115.0 environment: sdk: ">=3.3.0 <4.0.0" @@ -45,15 +45,15 @@ dependencies: freezed_annotation: ^2.4.1 get_it: ^7.6.4 http: ^1.1.2 - image_picker: ^1.0.5 + image_picker: ^1.1.1 injectable: ^2.3.2 intercom_flutter: ^9.0.2 intl: ^0.18.1 json_annotation: ^4.8.1 logging: ^1.2.0 meta: ^1.10.0 - mobile_scanner: ^5.0.1 - package_info_plus: ^7.0.0 + mobile_scanner: ^5.0.2 + package_info_plus: ^8.0.0 path: ^1.8.3 path_provider: ^2.1.1 permission_handler: ^11.1.0 @@ -63,7 +63,7 @@ dependencies: retrofit: ^4.0.3 rive: ^0.13.2 rxdart: ^0.27.7 - sentry_flutter: ^8.0.0 + sentry_flutter: ^8.1.0 share: ^2.0.4 shared_preferences: ^2.2.2 sliver_tools: ^0.2.12 diff --git a/packages/espressocash_app/tool/cycles.py b/packages/espressocash_app/tool/cycles.py index 97e0e15c79..c52b6c61e8 100644 --- a/packages/espressocash_app/tool/cycles.py +++ b/packages/espressocash_app/tool/cycles.py @@ -53,9 +53,10 @@ - initial script creation """ -import sys -from os import path, access, R_OK import argparse +import sys +from os import R_OK, access, path + import networkx as nx from networkx.drawing.nx_pydot import read_dot @@ -87,6 +88,9 @@ def main(): i.append(i[0]) print(" -> ".join(i)) + if len(C) != 0: + sys.exit(1) + def remove_super_cycles(cycle_list): # sorting by length makes the search easier, because shorter cycles cannot be supercycles of longer ones