diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 4df215e13..282941365 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -16,6 +16,7 @@ jobs: env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet + PR_NUMBER: ${{ github.event.number }} steps: - name: is pr @@ -150,10 +151,7 @@ jobs: - name: Rename app run: | - hash=`sha512sum <<<"${{ env.BRANCH_NAME }}"` - substring=${hash:0:15} - echo substring - echo -e "id=com.cakewallet.test_$(substring)\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties + echo -e "id=com.cakewallet.test_${{ env.PR_NUMBER }}\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties - name: Build run: | diff --git a/assets/images/notification_icon.png b/assets/images/notification_icon.png new file mode 100644 index 000000000..a6d60211a Binary files /dev/null and b/assets/images/notification_icon.png differ diff --git a/assets/images/status_website_image.png b/assets/images/status_website_image.png new file mode 100644 index 000000000..017bb64e1 Binary files /dev/null and b/assets/images/status_website_image.png differ diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index ce509015c..9cebce10a 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -216,7 +216,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); - static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 90, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); + static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); static final Map _rawCurrencyMap = diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index ab4bfb0b0..58f63d7ce 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -183,13 +183,8 @@ abstract class MoneroWalletBase // try to use the date instead: try { _setHeightFromDate(); - } catch (e, s) { + } catch (_) { // we still couldn't get a valid sync height :/ - onError?.call(FlutterErrorDetails( - exception: e, - stack: s, - library: this.runtimeType.toString(), - )); } } } @@ -287,7 +282,9 @@ abstract class MoneroWalletBase pendingTransactionDescription = await transaction_history.createTransaction( address: address!, amount: amount, - priorityRaw: _credentials.priority.serialize(), + priorityRaw: _credentials.priority == MoneroTransactionPriority.automatic + ? MoneroTransactionPriority.medium.serialize() + : _credentials.priority.serialize(), accountIndex: walletAddresses.account!.id, preferredInputs: inputs); } diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index f901a64be..3476d76cd 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -165,16 +165,6 @@ abstract class SolanaWalletBase throw Exception("Solana Node connection failed"); } - try { - await Future.wait([ - _updateBalance(), - _updateNativeSOLTransactions(), - _updateSPLTokenTransactions(), - ]); - } catch (e) { - log(e.toString()); - } - _setTransactionUpdateTimer(); syncStatus = ConnectedSyncStatus(); diff --git a/lib/di.dart b/lib/di.dart index 473eaed00..782c0f1f4 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -384,6 +384,7 @@ Future setup({ yatStore: getIt.get(), ordersStore: getIt.get(), anonpayTransactionsStore: getIt.get(), + sharedPreferences: getIt.get(), keyService: getIt.get())); getIt.registerFactory( diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 0dd251aaa..1808be97c 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -72,4 +72,5 @@ class PreferencesKey { static const lastSeenAppVersion = 'last_seen_app_version'; static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; static const isNewInstall = 'is_new_install'; + static const serviceStatusShaKey = 'service_status_sha_key'; } diff --git a/lib/entities/service_status.dart b/lib/entities/service_status.dart new file mode 100644 index 000000000..392c8072d --- /dev/null +++ b/lib/entities/service_status.dart @@ -0,0 +1,41 @@ +class ServiceStatus { + final String title; + final String description; + final String? image; + final String? status; + final DateTime date; + + ServiceStatus( + {required this.title, + required this.description, + required this.date, + this.image, + this.status}); + + factory ServiceStatus.fromJson(Map json) => ServiceStatus( + title: json['title'] as String? ?? '', + description: json['description'] as String? ?? '', + date: DateTime.tryParse(json['date'] as String? ?? '') ?? DateTime.now(), + image: json['image'] as String?, + status: json['status'] as String?, + ); +} + +class ServicesResponse { + final List servicesStatus; + final bool hasUpdates; + final String currentSha; + + ServicesResponse(this.servicesStatus, this.hasUpdates, this.currentSha); + + factory ServicesResponse.fromJson( + Map json, bool hasUpdates, String currentSha) { + return ServicesResponse( + (json['notices'] as List? ?? []) + .map((e) => ServiceStatus.fromJson(e as Map)) + .toList(), + hasUpdates, + currentSha, + ); + } +} diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index a86d6b0c6..9f9d81e5f 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -110,7 +110,7 @@ class CWSolana extends Solana { @override List? getValidationLength(CryptoCurrency type) { if (type is SPLToken) { - return [44]; + return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; } return null; diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 61e7d6176..3806dff91 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sideba import 'package:cake_wallet/src/screens/dashboard/pages/market_place_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; +import 'package:cake_wallet/src/widgets/services_updates_widget.dart'; import 'package:cake_wallet/src/widgets/vulnerable_seeds_popup.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/utils/device_info.dart'; @@ -101,6 +102,9 @@ class _DashboardPageView extends BasePage { @override Widget get endDrawer => MenuWidget(dashboardViewModel); + @override + Widget leading(BuildContext context) => ServicesUpdatesWidget(dashboardViewModel.getServicesStatus()); + @override Widget middle(BuildContext context) { return SyncIndicator( diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index b251e4b45..5c1111740 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -179,7 +179,7 @@ class BaseAlertDialog extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (headerText != null) headerTitle(context), + if (headerText?.isNotEmpty ?? false) headerTitle(context), Padding( padding: EdgeInsets.fromLTRB(24, 20, 24, 0), child: title(context), diff --git a/lib/src/widgets/service_status_tile.dart b/lib/src/widgets/service_status_tile.dart new file mode 100644 index 000000000..f2a079841 --- /dev/null +++ b/lib/src/widgets/service_status_tile.dart @@ -0,0 +1,71 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/entities/service_status.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class ServiceStatusTile extends StatelessWidget { + final ServiceStatus status; + + const ServiceStatusTile(this.status, {super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: const EdgeInsets.all(8), + title: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: AutoSizeText( + "${status.title}${status.status != null ? " - ${status.status}" : ""}", + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.start, + ), + ), + Text( + _getTimeString(status.date), + style: TextStyle(fontSize: 12), + ), + ], + ), + ), + leading: RotatedBox( + child: Icon( + Icons.info, + color: status.status == "resolved" ? Colors.green : Colors.red, + ), + quarterTurns: 2, + ), + subtitle: Row( + children: [ + Expanded(child: Text(status.description)), + if (status.image != null) + SizedBox( + height: 50, + width: 50, + child: Image.network(status.image!), + ), + ], + ), + ); + } + + String _getTimeString(DateTime date) { + int difference = DateTime.now().difference(date).inHours; + if (difference == 0) { + return "few minutes ago"; + } + if (difference < 24) { + return DateFormat('h:mm a').format(date); + } + return DateFormat('d-MM-yyyy').format(date); + } +} diff --git a/lib/src/widgets/services_updates_widget.dart b/lib/src/widgets/services_updates_widget.dart new file mode 100644 index 000000000..7a8614b75 --- /dev/null +++ b/lib/src/widgets/services_updates_widget.dart @@ -0,0 +1,120 @@ +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/entities/service_status.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/service_status_tile.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/wallet_list_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ServicesUpdatesWidget extends StatelessWidget { + final Future servicesResponse; + + const ServicesUpdatesWidget(this.servicesResponse, {super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: FutureBuilder( + future: servicesResponse, + builder: (context, state) { + return InkWell( + onTap: state.hasData + ? () { + // save currentSha when the user see the status + getIt + .get() + .setString(PreferencesKey.serviceStatusShaKey, state.data!.currentSha); + + showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(50), + topRight: Radius.circular(50), + ), + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height / 2, + minHeight: MediaQuery.of(context).size.height / 4, + ), + builder: (context) { + Widget body; + if (state.data!.servicesStatus.isEmpty) { + body = Center( + child: Text("Everything is up and running as expected"), + ); + } else { + body = SingleChildScrollView( + child: Column( + children: state.data!.servicesStatus + .map((status) => ServiceStatusTile(status)) + .toList()), + ); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 20), + child: Stack( + children: [ + body, + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width / 8), + child: PrimaryImageButton( + onPressed: () { + try { + launchUrl(Uri.parse("https://status.cakewallet.com/")); + } catch (_) {} + }, + image: Image.asset( + "assets/images/status_website_image.png", + color: Theme.of(context).brightness == Brightness.light + ? Colors.white + : null, + ), + text: "Status Website", + color: Theme.of(context) + .extension()! + .createNewWalletButtonBackgroundColor, + textColor: Theme.of(context) + .extension()! + .restoreWalletButtonTextColor, + ), + ), + ) + ], + ), + ); + }, + ); + } + : null, + child: Stack( + children: [ + Image.asset( + "assets/images/notification_icon.png", + color: Theme.of(context).extension()!.pageTitleTextColor, + ), + if (state.hasData && state.data!.hasUpdates) + Container( + height: 7, + width: 7, + margin: EdgeInsetsDirectional.only(start: 8), + decoration: BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index da5eb0373..a21a92cb4 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -4,8 +4,10 @@ import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/entities/service_status.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -42,7 +44,8 @@ import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/entities/provider_types.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; part 'dashboard_view_model.g.dart'; @@ -59,6 +62,7 @@ abstract class DashboardViewModelBase with Store { required this.yatStore, required this.ordersStore, required this.anonpayTransactionsStore, + required this.sharedPreferences, required this.keyService}) : hasSellAction = false, hasBuyAction = false, @@ -280,6 +284,7 @@ abstract class DashboardViewModelBase with Store { bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven; final KeyService keyService; + final SharedPreferences sharedPreferences; BalanceViewModel balanceViewModel; @@ -497,4 +502,26 @@ abstract class DashboardViewModelBase with Store { return affectedWallets; } + + Future getServicesStatus() async { + try { + final res = await http.get(Uri.parse("https://service-api.cakewallet.com/v1/active-notices")); + + if (res.statusCode < 200 || res.statusCode >= 300) { + throw res.body; + } + + final oldSha = sharedPreferences.getString(PreferencesKey.serviceStatusShaKey); + + + final hash = await Cryptography.instance.sha256().hash(utf8.encode(res.body)); + final currentSha = bytesToHex(hash.bytes); + + final hasUpdates = oldSha != currentSha; + + return ServicesResponse.fromJson(json.decode(res.body) as Map, hasUpdates, currentSha); + } catch (_) { + return ServicesResponse([], false, ''); + } + } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 772150368..507ed14cc 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -427,9 +427,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ) { if (walletType == WalletType.ethereum || walletType == WalletType.polygon || + walletType == WalletType.solana || walletType == WalletType.haven) { if (error.contains('gas required exceeds allowance') || - error.contains('insufficient funds for')) { + error.contains('insufficient funds')) { return S.current.do_not_have_enough_gas_asset(currency.toString()); } diff --git a/scripts/android/build_monero.sh b/scripts/android/build_monero.sh index e0010b351..7e9b58b6d 100755 --- a/scripts/android/build_monero.sh +++ b/scripts/android/build_monero.sh @@ -1,7 +1,7 @@ #!/bin/sh . ./config.sh -MONERO_BRANCH=release-v0.18.2.2-android +MONERO_BRANCH=release-v0.18.2.2-android_tx_priority_fix MONERO_SRC_DIR=${WORKDIR}/monero git clone https://github.com/cake-tech/monero.git ${MONERO_SRC_DIR} --branch ${MONERO_BRANCH} diff --git a/scripts/ios/build_monero.sh b/scripts/ios/build_monero.sh index 54dda546f..2c81576f1 100755 --- a/scripts/ios/build_monero.sh +++ b/scripts/ios/build_monero.sh @@ -4,7 +4,7 @@ MONERO_URL="https://github.com/cake-tech/monero.git" MONERO_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/monero" -MONERO_VERSION=release-v0.18.2.2 +MONERO_VERSION=release-v0.18.2.2_tx_priority_fix BUILD_TYPE=release PREFIX=${EXTERNAL_IOS_DIR} DEST_LIB_DIR=${EXTERNAL_IOS_LIB_DIR}/monero diff --git a/scripts/ios/build_sodium.sh b/scripts/ios/build_sodium.sh index 66f383fb4..5632719ad 100755 --- a/scripts/ios/build_sodium.sh +++ b/scripts/ios/build_sodium.sh @@ -8,9 +8,10 @@ SODIUM_URL="https://github.com/jedisct1/libsodium.git" echo "============================ SODIUM ============================" echo "Cloning SODIUM from - $SODIUM_URL" -git clone $SODIUM_URL $SODIUM_PATH --branch stable +git clone $SODIUM_URL $SODIUM_PATH cd $SODIUM_PATH -./dist-build/ios.sh +git checkout 443617d7507498f7477703f0b51cb596d4539262 +./dist-build/apple-xcframework.sh -mv ${SODIUM_PATH}/libsodium-ios/include/* $EXTERNAL_IOS_INCLUDE_DIR -mv ${SODIUM_PATH}/libsodium-ios/lib/* $EXTERNAL_IOS_LIB_DIR \ No newline at end of file +mv ${SODIUM_PATH}/libsodium-apple/ios/include/* $EXTERNAL_IOS_INCLUDE_DIR +mv ${SODIUM_PATH}/libsodium-apple/ios/lib/* $EXTERNAL_IOS_LIB_DIR \ No newline at end of file diff --git a/scripts/macos/build_monero.sh b/scripts/macos/build_monero.sh index c1ee0e5d3..2a1ae89a5 100755 --- a/scripts/macos/build_monero.sh +++ b/scripts/macos/build_monero.sh @@ -4,7 +4,7 @@ MONERO_URL="https://github.com/cake-tech/monero.git" MONERO_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/monero" -MONERO_VERSION=release-v0.18.2.2 +MONERO_VERSION=release-v0.18.2.2_tx_priority_fix BUILD_TYPE=release PREFIX=${EXTERNAL_MACOS_DIR} DEST_LIB_DIR=${EXTERNAL_MACOS_LIB_DIR}/monero