diff --git a/android/app/build.gradle b/android/app/build.gradle index e20d056499..3d4fd3eb77 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -104,8 +104,8 @@ android { applicationId "com.bitpay.wallet" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 91212431 - versionName "14.44.2" + versionCode 91212432 + versionName "14.45.0" missingDimensionStrategy 'react-native-camera', 'mlkit' ndk { abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a" diff --git a/assets/img/icon-alert-triangle.svg b/assets/img/icon-alert-triangle.svg new file mode 100644 index 0000000000..dc3298b998 --- /dev/null +++ b/assets/img/icon-alert-triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/icon-archive-download.svg b/assets/img/icon-archive-download.svg new file mode 100644 index 0000000000..4e0713a8ef --- /dev/null +++ b/assets/img/icon-archive-download.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/icon-clock-outline.svg b/assets/img/icon-clock-outline.svg new file mode 100644 index 0000000000..88ece94cbe --- /dev/null +++ b/assets/img/icon-clock-outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/icon-multisig-group.svg b/assets/img/icon-multisig-group.svg new file mode 100644 index 0000000000..dfc734935f --- /dev/null +++ b/assets/img/icon-multisig-group.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/icon-schedule-clock.svg b/assets/img/icon-schedule-clock.svg new file mode 100644 index 0000000000..da8b1809f8 --- /dev/null +++ b/assets/img/icon-schedule-clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/img/icon-verified-shield.svg b/assets/img/icon-verified-shield.svg new file mode 100644 index 0000000000..2f8369030e --- /dev/null +++ b/assets/img/icon-verified-shield.svg @@ -0,0 +1,3 @@ + + + diff --git a/declarations.d.ts b/declarations.d.ts index a7bf0794e9..f08caed29c 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -11,8 +11,6 @@ declare module '@env' { export const BRAZE_API_KEY_ANDROID: string; export const BRAZE_API_KEY_IOS: string; export const BRAZE_EXPORT_API_KEY: string; - export const BRAZE_MERGE_AND_DELETE_API_KEY: string; - export const BRAZE_REST_API_ENDPOINT: string; export const COINBASE_CLIENT_ID: string; export const COINBASE_CLIENT_SECRET: string; export const DISABLE_DEVELOPMENT_LOGGING: string; @@ -20,7 +18,6 @@ declare module '@env' { export const DOSH_WHITELIST: string; export const GIT_COMMIT_HASH: string; export const MIXPANEL_PROJECT_TOKEN: string; - export const MORALIS_API_KEY: string; export const WALLET_CONNECT_V2_PROJECT_ID: string; export const ZENLEDGER_CLIENT_ID: string; export const ZENLEDGER_CLIENT_SECRET: string; diff --git a/ios/BitPayApp/Info.plist b/ios/BitPayApp/Info.plist index c8e4890db2..09557eb3ca 100644 --- a/ios/BitPayApp/Info.plist +++ b/ios/BitPayApp/Info.plist @@ -26,7 +26,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 14.44.2 + 14.45.0 CFBundleSignature ???? CFBundleURLTypes diff --git a/package.json b/package.json index 0b125bc09d..f391fc07fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bitpay", - "version": "14.44.2", + "version": "14.45.0", "private": true, "engines": { "node": ">=20" @@ -48,7 +48,7 @@ "translation:download": "crowdin download --identity crowdin-credentials.yml" }, "dependencies": { - "@bitpay-labs/bitcore-wallet-client": "11.10.0", + "@bitpay-labs/bitcore-wallet-client": "11.10.6", "@braze/react-native-sdk": "20.1.0", "@ethersproject/shims": "5.7.0", "@freakycoder/react-native-bounceable": "0.2.5", @@ -113,7 +113,6 @@ "lodash.uniqby": "4.7.0", "mixpanel-react-native": "3.3.0", "moment": "2.29.1", - "moralis": "2.25.2", "papaparse": "5.3.2", "path-browserify": "1.0.1", "react": "19.1.1", diff --git a/patches/@bitpay-labs+bitcore-wallet-client+11.5.2.patch b/patches/@bitpay-labs+bitcore-wallet-client+11.10.0.patch similarity index 71% rename from patches/@bitpay-labs+bitcore-wallet-client+11.5.2.patch rename to patches/@bitpay-labs+bitcore-wallet-client+11.10.0.patch index 105fce6cbc..9863f4ad55 100644 --- a/patches/@bitpay-labs+bitcore-wallet-client+11.5.2.patch +++ b/patches/@bitpay-labs+bitcore-wallet-client+11.10.0.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@bitpay-labs/bitcore-wallet-client/ts_build/src/lib/tsskey.js b/node_modules/@bitpay-labs/bitcore-wallet-client/ts_build/src/lib/tsskey.js -index d073bca..cdeaea3 100644 +index b21da63..5075941 100644 --- a/node_modules/@bitpay-labs/bitcore-wallet-client/ts_build/src/lib/tsskey.js +++ b/node_modules/@bitpay-labs/bitcore-wallet-client/ts_build/src/lib/tsskey.js @@ -118,17 +118,19 @@ class TssKeyGen extends events_1.EventEmitter { @@ -23,3 +23,18 @@ index d073bca..cdeaea3 100644 __classPrivateFieldGet(this, _TssKeyGen_request, "f").setCredentials(__classPrivateFieldGet(this, _TssKeyGen_credentials, "f")); __classPrivateFieldSet(this, _TssKeyGen_requestPrivateKey, crypto_wallet_core_1.BitcoreLib.PrivateKey.fromString(__classPrivateFieldGet(this, _TssKeyGen_credentials, "f").requestPrivKey), "f"); const baseXPrivKey = __classPrivateFieldGet(this, _TssKeyGen_key, "f").get(params.password).xPrivKey; +diff --git a/node_modules/@bitpay-labs/bitcore-wallet-client/ts_build/src/lib/tsssign.js b/node_modules/@bitpay-labs/bitcore-wallet-client/ts_build/src/lib/tsssign.js +index f8e7463..f096163 100644 +--- a/node_modules/@bitpay-labs/bitcore-wallet-client/ts_build/src/lib/tsssign.js ++++ b/node_modules/@bitpay-labs/bitcore-wallet-client/ts_build/src/lib/tsssign.js +@@ -84,7 +84,9 @@ class TssSign extends events_1.EventEmitter { + } + async restoreSession(params) { + const { session } = params; +- const [id, sigSession] = session.split(':'); ++ const lastColon = session.lastIndexOf(':'); ++ const id = session.slice(0, lastColon); ++ const sigSession = session.slice(lastColon + 1); + this.id = id; + __classPrivateFieldSet(this, _TssSign_sign, await bitcore_tss_1.ECDSA.Sign.restore({ + session: sigSession, diff --git a/patches/react-native-worklets+0.8.1.patch b/patches/react-native-worklets+0.8.1.patch new file mode 100644 index 0000000000..2e76e30700 --- /dev/null +++ b/patches/react-native-worklets+0.8.1.patch @@ -0,0 +1,226 @@ +diff --git a/node_modules/react-native-worklets/Common/cpp/worklets/Registries/WorkletRuntimeRegistry.cpp b/node_modules/react-native-worklets/Common/cpp/worklets/Registries/WorkletRuntimeRegistry.cpp +index 61fe6fa..d9a031b 100644 +--- a/node_modules/react-native-worklets/Common/cpp/worklets/Registries/WorkletRuntimeRegistry.cpp ++++ b/node_modules/react-native-worklets/Common/cpp/worklets/Registries/WorkletRuntimeRegistry.cpp +@@ -5,6 +5,6 @@ + namespace worklets { + + std::set WorkletRuntimeRegistry::registry_{}; +-std::mutex WorkletRuntimeRegistry::mutex_{}; ++std::shared_mutex WorkletRuntimeRegistry::mutex_{}; + + } // namespace worklets +diff --git a/node_modules/react-native-worklets/Common/cpp/worklets/Registries/WorkletRuntimeRegistry.h b/node_modules/react-native-worklets/Common/cpp/worklets/Registries/WorkletRuntimeRegistry.h +index d97ed29..ec93a41 100644 +--- a/node_modules/react-native-worklets/Common/cpp/worklets/Registries/WorkletRuntimeRegistry.h ++++ b/node_modules/react-native-worklets/Common/cpp/worklets/Registries/WorkletRuntimeRegistry.h +@@ -3,8 +3,9 @@ + #include + #include + +-#include + #include ++#include ++#include + + using namespace facebook; + +@@ -13,27 +14,29 @@ namespace worklets { + class WorkletRuntimeRegistry { + private: + static std::set registry_; +- static std::mutex mutex_; // Protects `registry_`. ++ static std::shared_mutex mutex_; + + WorkletRuntimeRegistry() {} // private ctor + + static void registerRuntime(jsi::Runtime &runtime) { +- std::lock_guard lock(mutex_); ++ std::lock_guard lock(mutex_); + registry_.insert(&runtime); + } + + static void unregisterRuntime(jsi::Runtime &runtime) { +- std::lock_guard lock(mutex_); ++ std::lock_guard lock(mutex_); + registry_.erase(&runtime); + } + + friend class WorkletRuntimeCollector; + + public: +- static bool isRuntimeAlive(jsi::Runtime *runtime) { ++ template ++ static void runWhileLocked(jsi::Runtime *runtime, TFn &&fn) { + react_native_assert(runtime != nullptr && "runtime is nullptr"); +- std::lock_guard lock(mutex_); +- return registry_.find(runtime) != registry_.end(); ++ std::shared_lock lock(mutex_); ++ const bool isAlive = registry_.find(runtime) != registry_.end(); ++ std::forward(fn)(isAlive); + } + }; + +diff --git a/node_modules/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.h b/node_modules/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.h +index 1c20158..a7009f6 100644 +--- a/node_modules/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.h ++++ b/node_modules/react-native-worklets/Common/cpp/worklets/SharedItems/Serializable.h +@@ -6,6 +6,7 @@ + #include + + #include ++#include + #include + #include + #include +@@ -16,35 +17,37 @@ namespace worklets { + + jsi::Function getValueUnpacker(jsi::Runtime &rt); + +-inline void cleanupIfRuntimeExists(jsi::Runtime *rt, std::unique_ptr &value) { +- if (rt != nullptr && !WorkletRuntimeRegistry::isRuntimeAlive(rt)) { +- // The below use of unique_ptr.release prevents the smart pointer from +- // calling the destructor of the kept object. This effectively results in +- // leaking some memory. We do this on purpose, as sometimes we would keep +- // references to JSI objects past the lifetime of its runtime (e.g., +- // shared values references from the RN VM holds reference to JSI objects +- // on the UI runtime). When the UI runtime is terminated, the orphaned JSI +- // objects would crash the app when their destructors are called, because +- // they call into a memory that's managed by the terminated runtime. We +- // accept the tradeoff of leaking memory here, as it has a limited impact. +- // This scenario can only occur when the React instance is torn down which +- // happens in development mode during app reloads, or in production when +- // the app is being shut down gracefully by the system. An alternative +- // solution would require us to keep track of all JSI values that are in +- // use which would require additional data structure and compute spent on +- // bookkeeping that only for the sake of destroying the values in time +- // before the runtime is terminated. Note that the underlying memory that +- // jsi::Value refers to is managed by the VM and gets freed along with the +- // runtime. +- value.release(); // NOLINT ++// Frees the heap-allocated jsi::Value wrapper without running ~jsi::Value. ++// Use when the runtime that owns the JSI handle is already gone. When the ++// owning runtime is terminated, the orphaned JSI objects would crash the app ++// if their destructors ran, because they call into memory managed by the ++// terminated runtime. The JS object itself lived inside the runtime's heap ++// and was reclaimed with the runtime; only the C++ wrapper allocation ++// remains, and we free it here without invoking ~jsi::Value. ++// See https://github.com/facebook/hermes/blob/75b617a/API/jsi/jsi/jsi.h#L833 ++inline void freeWithoutCallingDestructor(std::unique_ptr &value) { ++ ::operator delete(value.release()); ++} ++ ++inline void cleanupRuntimeAware(jsi::Runtime *rt, std::unique_ptr &value) { ++ if (value == nullptr || rt == nullptr) { ++ return; + } ++ WorkletRuntimeRegistry::runWhileLocked(rt, [&value](bool isAlive) { ++ if (isAlive) { ++ value.reset(); ++ } else { ++ freeWithoutCallingDestructor(value); ++ } ++ }); + } + + template + class RetainingSerializable : virtual public BaseClass { + private: + jsi::Runtime *primaryRuntime_; +- jsi::Runtime *secondaryRuntime_; ++ std::mutex secondaryCacheMutex_; ++ jsi::Runtime *secondaryRuntime_{nullptr}; + std::unique_ptr secondaryValue_; + + public: +@@ -62,20 +65,55 @@ class RetainingSerializable : virtual public BaseClass { + // shared value is created and then accessed on the same runtime + return BaseClass::toJSValue(rt); + } +- if (secondaryValue_ == nullptr) { +- auto value = BaseClass::toJSValue(rt); +- secondaryValue_ = std::make_unique(rt, value); +- secondaryRuntime_ = &rt; ++ ++ bool hasDifferentRuntimeCache = false; ++ { ++ // The same retained graph can be materialized concurrently by multiple ++ // Worklet Runtimes, so all secondary-cache access is synchronized. ++ std::lock_guard lock(secondaryCacheMutex_); ++ if (secondaryValue_ != nullptr) { ++ if (&rt == secondaryRuntime_) { ++ return jsi::Value(rt, *secondaryValue_); ++ } ++ ++ // The first secondary runtime remains the cache owner. A different ++ // runtime must materialize an uncached, runtime-affine representation. ++ hasDifferentRuntimeCache = true; ++ } ++ } ++ ++ // Materialization can recursively visit nested serializables and perform ++ // JSI operations, so it must not run while this cache mutex is held. ++ auto value = BaseClass::toJSValue(rt); ++ if (hasDifferentRuntimeCache) { + return value; + } +- if (&rt == secondaryRuntime_) { +- return jsi::Value(rt, *secondaryValue_); ++ ++ auto candidate = std::make_unique(rt, value); ++ ++ { ++ std::lock_guard lock(secondaryCacheMutex_); ++ if (secondaryValue_ == nullptr) { ++ secondaryRuntime_ = &rt; ++ secondaryValue_ = std::move(candidate); ++ } + } +- return BaseClass::toJSValue(rt); ++ ++ // If another runtime published first, `candidate` is destroyed here on ++ // the runtime thread where it was created and `value` stays runtime-correct. ++ return value; + } + + ~RetainingSerializable() { +- cleanupIfRuntimeExists(secondaryRuntime_, secondaryValue_); ++ jsi::Runtime *secondaryRuntime = nullptr; ++ std::unique_ptr secondaryValue; ++ { ++ std::lock_guard lock(secondaryCacheMutex_); ++ secondaryRuntime = secondaryRuntime_; ++ secondaryRuntime_ = nullptr; ++ secondaryValue = std::move(secondaryValue_); ++ } ++ cleanupRuntimeAware(secondaryRuntime, secondaryValue); + } + }; + +@@ -246,7 +284,7 @@ class SerializableRemoteFunction : public Serializable, + } + + ~SerializableRemoteFunction() override { +- cleanupIfRuntimeExists(runtime_, function_); ++ cleanupRuntimeAware(runtime_, function_); + } + + jsi::Value toJSValue(jsi::Runtime &rt) override; +@@ -261,7 +299,7 @@ class SerializableInitializer : public Serializable { + std::unique_ptr initializer_; + std::unique_ptr remoteValue_; + mutable std::mutex initializationMutex_; +- jsi::Runtime *remoteRuntime_; ++ jsi::Runtime *remoteRuntime_{nullptr}; + + public: + SerializableInitializer(jsi::Runtime &rt, const jsi::Object &initializerObject) +@@ -269,7 +307,7 @@ class SerializableInitializer : public Serializable { + initializer_(std::make_unique(rt, initializerObject)) {} + + ~SerializableInitializer() override { +- cleanupIfRuntimeExists(remoteRuntime_, remoteValue_); ++ cleanupRuntimeAware(remoteRuntime_, remoteValue_); + } + + jsi::Value toJSValue(jsi::Runtime &rt) override; diff --git a/scripts/check-abi-filters.js b/scripts/check-abi-filters.js new file mode 100644 index 0000000000..a7245afc8b --- /dev/null +++ b/scripts/check-abi-filters.js @@ -0,0 +1,52 @@ +// scripts/check-abi-filters.js +const fs = require('fs'); +const path = require('path'); + +const gradlePath = path.join(process.cwd(), 'android', 'app', 'build.gradle'); +const gradleLines = fs.readFileSync(gradlePath, 'utf8').split('\n'); + +const propsPath = path.join(process.cwd(), 'android', 'gradle.properties'); +const propsLines = fs.readFileSync(propsPath, 'utf8').split('\n'); + +const checks = [ + { + file: 'android/app/build.gradle', + description: 'reactNativeArchitectures() default return', + lines: gradleLines, + match: line => + line.includes('return value ? value.split') && + line.includes('armeabi-v7a'), + }, + { + file: 'android/app/build.gradle', + description: 'ndk { abiFilters }', + lines: gradleLines, + match: line => line.includes('abiFilters') && line.includes('armeabi-v7a'), + }, + { + file: 'android/app/build.gradle', + description: 'versionCodes map', + lines: gradleLines, + match: line => + line.includes('def versionCodes') && line.includes('armeabi-v7a'), + }, + { + file: 'android/gradle.properties', + description: 'reactNativeArchitectures property', + lines: propsLines, + match: line => + line.replace(/\s/g, '').startsWith('reactNativeArchitectures=') && + line.includes('armeabi-v7a'), + }, +]; + +const failed = checks.filter(check => !check.lines.some(check.match)); + +if (failed.length > 0) { + console.error('\narmeabi-v7a is missing in:'); + failed.forEach(c => console.error(` - ${c.file} → ${c.description}`)); + console.error('\n See PR #2078 for reference\n'); + process.exit(1); +} + +console.log(`ABI filters check passed (${checks.length}/${checks.length})`); diff --git a/scripts/postinstall.sh b/scripts/postinstall.sh index 68d873062d..06f9436b54 100755 --- a/scripts/postinstall.sh +++ b/scripts/postinstall.sh @@ -5,3 +5,4 @@ node ./scripts/mute-require-cycle-warnings.js node ./scripts/multi-modal-patch.js node ./scripts/git-commit-hash.js node ./scripts/generate-dkls-vendor.js +node ./scripts/check-abi-filters.js diff --git a/src/components/home-card/HomeCard.tsx b/src/components/home-card/HomeCard.tsx index 46d5ee6b02..332ef2f3eb 100644 --- a/src/components/home-card/HomeCard.tsx +++ b/src/components/home-card/HomeCard.tsx @@ -23,6 +23,8 @@ import {shouldScale} from '../../utils/helper-methods'; import {useTranslation} from 'react-i18next'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; import ArrowRightSvg from '../../navigation/tabs/home/components/ArrowRightSvg'; +import ThresholdBadge from '../threshold-badge/ThresholdBadge'; +import MultisigBadge from '../multisig-badge/MultisigBadge'; interface BodyProps { title?: string; @@ -34,6 +36,8 @@ interface BodyProps { percentageSuffix?: string; hideKeyBalance: boolean; pendingTssSession?: boolean; + tssMetadata?: {m: number; n: number}; + isMultisig?: boolean; } interface HomeCardProps { @@ -112,10 +116,16 @@ const HomeCard: React.FC = ({body, footer, onCTAPress}) => { percentageSuffix, hideKeyBalance, pendingTssSession, + tssMetadata, + isMultisig, } = body; const BodyComp = ( + {tssMetadata ? ( + + ) : null} + {isMultisig ? : null} {title && {title}} {needsBackup && !pendingTssSession ? ( diff --git a/src/components/multisig-badge/MultisigBadge.tsx b/src/components/multisig-badge/MultisigBadge.tsx new file mode 100644 index 0000000000..2b58a3a6b3 --- /dev/null +++ b/src/components/multisig-badge/MultisigBadge.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import {StyleProp, ViewStyle} from 'react-native'; +import {useTheme} from 'styled-components/native'; +import {useTranslation} from 'react-i18next'; +import {SlateDark, White} from '../../styles/colors'; +import MultisigIcon from '../../../assets/img/icon-multisig-group.svg'; +import WalletTypeBadge, { + WalletTypeBadgeSize, +} from '../wallet-type-badge/WalletTypeBadge'; + +interface Props { + size?: WalletTypeBadgeSize; + style?: StyleProp; +} + +const MultisigBadge: React.FC = ({size = 'list', style}) => { + const {t} = useTranslation(); + const theme = useTheme(); + const iconSize = size === 'card' ? 20 : 16; + + return ( + + } + label={t('Multisig')} + /> + ); +}; + +export default MultisigBadge; diff --git a/src/components/threshold-badge/ThresholdBadge.tsx b/src/components/threshold-badge/ThresholdBadge.tsx new file mode 100644 index 0000000000..8b23b0e434 --- /dev/null +++ b/src/components/threshold-badge/ThresholdBadge.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import {StyleProp, ViewStyle} from 'react-native'; +import styled from 'styled-components/native'; +import {useTranslation} from 'react-i18next'; +import {Warning, Warning25} from '../../styles/colors'; +import {BaseText} from '../styled/Text'; +import WalletTypeBadge, { + WalletTypeBadgeSize, +} from '../wallet-type-badge/WalletTypeBadge'; + +interface Props { + m: number; + n: number; + size?: WalletTypeBadgeSize; + style?: StyleProp; +} + +const BetaBadge = styled.View` + background-color: ${Warning25}; + border-radius: 100px; + padding: 2px 4px; +`; + +const BetaBadgeText = styled(BaseText)` + color: ${Warning}; + font-size: 10px; + line-height: 13px; +`; + +const ThresholdBadge: React.FC = ({m, n, size = 'list', style}) => { + const {t} = useTranslation(); + + return ( + + {t('Beta')} + + } + label={`${t('Threshold')} ${m}/${n}`} + /> + ); +}; + +export default ThresholdBadge; diff --git a/src/components/wallet-type-badge/WalletTypeBadge.tsx b/src/components/wallet-type-badge/WalletTypeBadge.tsx new file mode 100644 index 0000000000..7454e99f7c --- /dev/null +++ b/src/components/wallet-type-badge/WalletTypeBadge.tsx @@ -0,0 +1,47 @@ +import React, {ReactNode} from 'react'; +import {StyleProp, ViewStyle} from 'react-native'; +import styled from 'styled-components/native'; +import {BaseText} from '../styled/Text'; +import {LightBlack, Slate30, SlateDark} from '../../styles/colors'; + +export type WalletTypeBadgeSize = 'list' | 'card'; + +interface Props { + icon: ReactNode; + label: string; + size?: WalletTypeBadgeSize; + style?: StyleProp; +} + +const Container = styled.View` + flex-direction: row; + align-items: center; + align-self: flex-start; + gap: 4px; + border-width: 1px; + border-color: ${({theme: {dark}}) => (dark ? LightBlack : Slate30)}; + border-radius: 16px; + padding: 4px 8px; +`; + +const Label = styled(BaseText)<{size: WalletTypeBadgeSize}>` + font-style: normal; + font-weight: 400; + font-size: ${({size}) => (size === 'card' ? 13 : 12)}px; + line-height: ${({size}) => (size === 'card' ? 20 : 15)}px; + color: ${({theme: {dark}}) => (dark ? Slate30 : SlateDark)}; +`; + +const WalletTypeBadge: React.FC = ({ + icon, + label, + size = 'list', + style, +}) => ( + + {icon} + + +); + +export default WalletTypeBadge; diff --git a/src/constants/config.ts b/src/constants/config.ts index 25166cf799..820a6108bf 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -40,7 +40,7 @@ export const APP_NAME = 'bitpay'; export const APP_NAME_UPPERCASE = 'BitPay'; export const APP_NETWORK = Network.mainnet; export const APP_VERSION = version; -export const BASE_BITPAY_URLS = { +export const BASE_BITPAY_URLS: {[key: string]: string} = { [Network.mainnet]: 'https://bitpay.com', [Network.testnet]: 'https://test.bitpay.com', [Network.regtest]: REGTEST_BASE_BITPAY_URL || '', @@ -133,7 +133,7 @@ export const BLOCKCHAIN_EXPLORERS: {[key in string]: any} = { }, }; -export const METHOD_ENVS = { +export const METHOD_ENVS: {[key: string]: string} = { [Network.mainnet]: 'production', [Network.testnet]: 'dev', [Network.regtest]: 'dev', diff --git a/src/lib/Braze/index.ts b/src/lib/Braze/index.ts index 595174dd38..fffaee3f7f 100644 --- a/src/lib/Braze/index.ts +++ b/src/lib/Braze/index.ts @@ -1,11 +1,8 @@ import Braze from '@braze/react-native-sdk'; -import axios from 'axios'; import { BRAZE_API_ENDPOINT, BRAZE_API_KEY_ANDROID, BRAZE_API_KEY_IOS, - BRAZE_MERGE_AND_DELETE_API_KEY, - BRAZE_REST_API_ENDPOINT, } from '@env'; import {checkNotifications, RESULTS} from 'react-native-permissions'; import {NativeModules, Platform} from 'react-native'; @@ -105,52 +102,6 @@ const setUserAttributes = (attributes: BrazeUserAttributes) => { }); }; -const mergeUsers = async ( - user_to_merge: string, - user_to_keep: string, -): Promise => { - const url = 'https://' + BRAZE_REST_API_ENDPOINT + '/users/merge'; - const body = { - merge_updates: [ - { - identifier_to_merge: { - external_id: user_to_merge, - }, - identifier_to_keep: { - external_id: user_to_keep, - }, - }, - ], - }; - const headers = { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + BRAZE_MERGE_AND_DELETE_API_KEY, - }; - try { - const {data} = await axios.post(url, body, {headers}); - return data; - } catch (err: any) { - throw err.response?.data?.message || err.message || err; - } -}; - -const deleteUser = async (eid: string): Promise => { - const url = 'https://' + BRAZE_REST_API_ENDPOINT + '/users/delete'; - const body = { - external_ids: [eid], - }; - const headers = { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + BRAZE_MERGE_AND_DELETE_API_KEY, - }; - try { - const {data} = await axios.post(url, body, {headers}); - return data; - } catch (err: any) { - throw err.response?.data?.message || err.message || err; - } -}; - export type BrazeUserAttributes = { [K in (typeof nonCustomAttributes)[number]]?: string; } & Record; @@ -258,14 +209,6 @@ class BrazeClientWrapper { }; } - merge(userToMerge: string, userToKeep: string) { - return mergeUsers(userToMerge, userToKeep); - } - - delete(eid: string) { - return deleteUser(eid); - } - async screen(name: string, properties: Record = {}) { if (!(await this.ensureReady())) { return; diff --git a/src/navigation/services/components/externalServicesWalletSelector.tsx b/src/navigation/services/components/externalServicesWalletSelector.tsx index c0e9d37908..24ff391ea1 100644 --- a/src/navigation/services/components/externalServicesWalletSelector.tsx +++ b/src/navigation/services/components/externalServicesWalletSelector.tsx @@ -113,6 +113,7 @@ interface ExternalServicesWalletSelectorScreenProps { sellCryptoSupportedCoinsFullObj?: SellCryptoCoin[] | undefined; onWalletSelected?: (wallet: Wallet) => void; fromWallet?: Wallet; + fromAccount?: {keyId: string; accountAddress: string}; // used when entering from an EVM/SVM account (AccountDetails) currencyAbbreviation?: string | undefined; // used from charts and deeplinks. chain?: string | undefined; // used from charts and deeplinks. partner?: BuyCryptoExchangeKey | undefined; // used from deeplinks. @@ -131,6 +132,7 @@ const ExternalServicesWalletSelector: React.FC< sellCryptoSupportedCoins, onWalletSelected, fromWallet, + fromAccount, currencyAbbreviation, chain, loading, @@ -188,6 +190,22 @@ const ExternalServicesWalletSelector: React.FC< } }; + const isWalletBuySupported = (wallet: Wallet): boolean => { + return ( + wallet.credentials && + wallet.network === 'livenet' && + wallet.isComplete() && + !wallet.hideWallet && + !wallet.hideWalletByAccount && + buyCryptoSupportedCoins.includes( + getExternalServiceSymbol( + wallet.currencyAbbreviation.toLowerCase(), + wallet.chain, + ), + ) + ); + }; + const selectFirstAvailableWallet = async () => { const keysList: Key[] = Object.values(allKeys).filter( key => key.backupComplete, @@ -199,6 +217,47 @@ const ExternalServicesWalletSelector: React.FC< return; } + if ( + context === 'buyCrypto' && + !preSetWallet?.id && + !fromCurrencyAbbreviation + ) { + let scopedWallets: Wallet[]; + if (fromAccount?.keyId && fromAccount?.accountAddress) { + const accountKey = keysList.find(k => k.id === fromAccount.keyId); + scopedWallets = (accountKey?.wallets || []).filter( + w => w.receiveAddress === fromAccount.accountAddress, + ); + } else { + scopedWallets = keysList.flatMap(k => k.wallets || []); + } + const candidateWallets = scopedWallets.filter(isWalletBuySupported); + + const lastPurchaseWalletId = buyCryptoOpts?.lastPurchaseData?.walletId; + const lastUsedWallet = lastPurchaseWalletId + ? candidateWallets.find(w => w.id === lastPurchaseWalletId) + : undefined; + if (lastUsedWallet) { + setWallet(lastUsedWallet); + return; + } + + const fundedWallets = candidateWallets.filter( + w => (w.balance?.sat || 0) > 0, + ); + if (fundedWallets[0]) { + const [highestBalanceWallet] = orderBy( + fundedWallets, + w => w.balance?.fiat || 0, + 'desc', + ); + _setSelectedWallet(highestBalanceWallet); + return; + } + + return; + } + if (preSetWallet?.id || buyCryptoOpts?.lastPurchaseData?.walletId) { // Selected wallet from Wallet Details let fromWalletData; diff --git a/src/navigation/services/screens/BuyAndSellRoot.tsx b/src/navigation/services/screens/BuyAndSellRoot.tsx index 342e724ca1..7352ab10ae 100644 --- a/src/navigation/services/screens/BuyAndSellRoot.tsx +++ b/src/navigation/services/screens/BuyAndSellRoot.tsx @@ -465,6 +465,7 @@ export interface BuyAndSellRootProps { context: ExternalServicesContext; fromWallet?: Wallet; + fromAccount?: {keyId: string; accountAddress: string}; // used when entering from an EVM/SVM account (AccountDetails) amount?: number; // deeplink params are strings, ensure this is number so offers will work currencyAbbreviation?: string; // used from charts and deeplinks. chain?: string; // used from charts and deeplinks. @@ -531,6 +532,7 @@ const BuyAndSellRoot = ({ // Real route params const context = route.params?.context; const fromWallet = route.params?.fromWallet; + const fromAccount = route.params?.fromAccount; const fromAmount = useMemo(() => { const DEFAULT_USD_VALUE = 200; @@ -1096,8 +1098,7 @@ const BuyAndSellRoot = ({ } // TODO: add the ability to remove coins or chains from buyCryptoConfig - const coinsToRemove = - !locationData || locationData.countryShortCode === 'US' ? ['xrp'] : []; + const coinsToRemove: string[] = []; if (coinsToRemove.length > 0) { coinsToRemove.forEach((coin: string) => { @@ -1717,11 +1718,7 @@ const BuyAndSellRoot = ({ }); if (allSupportedCoins.length > 0) { - const coinsToRemove = - !locationData || locationData.countryShortCode === 'US' - ? ['xrp'] - : []; - coinsToRemove.push('busd'); + const coinsToRemove: string[] = ['busd']; if (coinsToRemove.length > 0) { logger.debug( @@ -2828,14 +2825,14 @@ const BuyAndSellRoot = ({ env: rampEnv, hostLogoUrl: 'https://bitpay.com/_nuxt/img/bitpay-logo-blue.1c0494b.svg', hostAppName: APP_NAME_UPPERCASE, - swapAsset: getRampCoinFormat(coin, getRampChainFormat(chain)), - swapAmount: offer.amountReceivingUnit!, - fiatCurrency: offer.fiatCurrency, + enabledCryptoAssets: getRampCoinFormat(coin, getRampChainFormat(chain)), + outAssetValue: offer.amountReceivingUnit!, + inAsset: offer.fiatCurrency, enabledFlows: ['ONRAMP'], defaultFlow: 'ONRAMP', userAddress: address, selectedCountryCode: country, - defaultAsset: getRampCoinFormat(coin, getRampChainFormat(chain)), + outAsset: getRampCoinFormat(coin, getRampChainFormat(chain)), finalUrl: redirectUrl, paymentMethodType: getRampPaymentMethodFormat(paymentMethod.method), }; @@ -3535,18 +3532,16 @@ const BuyAndSellRoot = ({ userAddress: address, // TODO: ask Ramp team about this for UTXO coins hostLogoUrl: 'https://bitpay.com/_nuxt/img/bitpay-logo-blue.1c0494b.svg', hostAppName: APP_NAME_UPPERCASE, - offrampAsset: rampAsset, - swapAsset: rampAsset, - swapAmount: offer.decimals + enabledCryptoAssets: rampAsset, + inAssetValue: offer.decimals ? Number(offer.sellAmount) * 10 ** offer.decimals : undefined, - fiatCurrency: offer.fiatCurrency, + outAsset: offer.fiatCurrency, enabledFlows: 'OFFRAMP', defaultFlow: 'OFFRAMP', selectedCountryCode: country, - defaultAsset: rampAsset, + inAsset: rampAsset, useSendCryptoCallback: true, - useSendCryptoCallbackVersion: 1, hideExitButton: false, }; @@ -4154,6 +4149,7 @@ const BuyAndSellRoot = ({ sellCryptoSupportedCoinsFullObj ?? [] } fromWallet={fromWallet} + fromAccount={fromAccount} currencyAbbreviation={fromCurrencyAbbreviation} chain={fromChain} partner={preSetPartner} diff --git a/src/navigation/services/screens/MoonpayBuyEmbeddedCheckout.tsx b/src/navigation/services/screens/MoonpayBuyEmbeddedCheckout.tsx index ee04929432..4e59bed68b 100644 --- a/src/navigation/services/screens/MoonpayBuyEmbeddedCheckout.tsx +++ b/src/navigation/services/screens/MoonpayBuyEmbeddedCheckout.tsx @@ -752,7 +752,7 @@ const MoonpayBuyEmbeddedCheckout: React.FC = () => { // copyText(toAddress); }} /> - + */} {isLoading ? ( diff --git a/src/navigation/services/swap-crypto/screens/SwapCryptoRoot.tsx b/src/navigation/services/swap-crypto/screens/SwapCryptoRoot.tsx index 69bc8c3ab3..2f3914c745 100644 --- a/src/navigation/services/swap-crypto/screens/SwapCryptoRoot.tsx +++ b/src/navigation/services/swap-crypto/screens/SwapCryptoRoot.tsx @@ -64,6 +64,7 @@ import { getChainFromChangellyBlockchain, changellyCreateFixTransaction, changellyGetFixRateForAmount, + generateMessageId, } from '../utils/changelly-utils'; import {useAppDispatch, useAppSelector} from '../../../../utils/hooks'; import { @@ -96,10 +97,7 @@ import { TSSSigningStatus, TSSSigningProgress, } from '../../../../store/wallet/wallet.models'; -import { - changellyGetCurrencies, - changellyGetTransactions, -} from '../../../../store/swap-crypto/effects/changelly/changelly'; +import {changellyGetCurrencies} from '../../../../store/swap-crypto/effects/changelly/changelly'; import { dismissBottomNotificationModal, showBottomNotificationModal, @@ -1698,11 +1696,7 @@ const SwapCryptoRoot: React.FC = () => { } }); if (allSupportedCoins.length > 0) { - const coinsToRemove = - !locationData || locationData.countryShortCode === 'US' - ? ['xrp'] - : []; - coinsToRemove.push('busd'); + const coinsToRemove: string[] = ['busd']; if (coinsToRemove.length > 0) { logger.debug( @@ -2047,9 +2041,12 @@ const SwapCryptoRoot: React.FC = () => { apiExtraFee = Number(data.result.apiExtraFee); } else { try { - const transactionData = await changellyGetTransactions( - data.result.id, - ); + const _txRaw = await fromWalletSelected.changellyGetTransactions({ + id: generateMessageId(fromWalletSelected.id), + exchangeTxId: data.result.id, + useV2: true, + }); + const transactionData = _txRaw?.body ?? _txRaw; if (transactionData.result[0]) { if (Number(transactionData.result[0].changellyFee) > 0) { changellyFee = Number(transactionData.result[0].changellyFee); diff --git a/src/navigation/tabs/TabScreenErrorFallback.tsx b/src/navigation/tabs/TabScreenErrorFallback.tsx index 3e286b81f0..b4dc0feb55 100644 --- a/src/navigation/tabs/TabScreenErrorFallback.tsx +++ b/src/navigation/tabs/TabScreenErrorFallback.tsx @@ -2,6 +2,7 @@ import React, {useState} from 'react'; import {useTranslation} from 'react-i18next'; import {Linking, ScrollView} from 'react-native'; import ErrorBoundary from 'react-native-error-boundary'; +import * as Sentry from '@sentry/react-native'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; import {useNavigation} from '@react-navigation/native'; import {NativeStackScreenProps} from '@react-navigation/native-stack'; @@ -153,6 +154,14 @@ export const withErrorFallback = ( { + Sentry.captureException(err, { + level: 'error', + tags: { + errorBoundary: 'tab-screen', + screen: TabScreen.displayName || TabScreen.name || 'unknown', + }, + extra: {componentStack: stack}, + }); setError(err); setStackTrace(stack); }}> diff --git a/src/navigation/tabs/home/components/Crypto.tsx b/src/navigation/tabs/home/components/Crypto.tsx index a30ada508f..039da6860a 100644 --- a/src/navigation/tabs/home/components/Crypto.tsx +++ b/src/navigation/tabs/home/components/Crypto.tsx @@ -69,6 +69,7 @@ import {t} from 'i18next'; import {Analytics} from '../../../../store/analytics/analytics.effects'; import AddSvg from './AddSvg'; import {isTSSKey} from '../../../../store/wallet/effects/tss-send/tss-send'; +import {IsShared} from '../../../../store/wallet/effects/transactions/transactions'; import {logManager} from '../../../../managers/LogManager'; import {WalletScreens} from '../../../../navigation/wallet/WalletGroup'; import {IsVMChain} from '../../../../store/wallet/utils/currency'; @@ -297,6 +298,8 @@ export const createHomeCardList = ({ const fullWalletObj = key?.wallets?.[0]; const hasPendingTssSession = fullWalletObj?.pendingTssSession; + const tssMetadata = key.wallets?.find(w => w.tssKeyId)?.tssMetadata; + const isMultisig = !!key.wallets?.some(w => IsShared(w)); return { id: key.id, @@ -311,6 +314,8 @@ export const createHomeCardList = ({ needsBackup={!backupComplete} context={context} pendingTssSession={hasPendingTssSession} + tssMetadata={tssMetadata} + isMultisig={isMultisig} onPress={ onPress ? () => { diff --git a/src/navigation/tabs/home/components/Wallet.tsx b/src/navigation/tabs/home/components/Wallet.tsx index c629aecb44..94a737823c 100644 --- a/src/navigation/tabs/home/components/Wallet.tsx +++ b/src/navigation/tabs/home/components/Wallet.tsx @@ -33,6 +33,8 @@ import ArrowRightSvg from './ArrowRightSvg'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; import {maskIfHidden} from '../../../../utils/hideBalances'; import {isUnitedKingdomCountry} from '../../../../store/location/location.effects'; +import ThresholdBadge from '../../../../components/threshold-badge/ThresholdBadge'; +import MultisigBadge from '../../../../components/multisig-badge/MultisigBadge'; interface WalletCardComponentProps { wallets: Wallet[]; @@ -45,6 +47,8 @@ interface WalletCardComponentProps { hideKeyBalance: boolean; context?: 'keySelector'; pendingTssSession?: boolean; + tssMetadata?: {m: number; n: number}; + isMultisig?: boolean; } export const HeaderImg = styled.View` @@ -203,6 +207,8 @@ const WalletCardComponent: React.FC = ({ layout, context, pendingTssSession, + tssMetadata, + isMultisig, }) => { const {t} = useTranslation(); const defaultAltCurrency = useAppSelector(({APP}) => APP.defaultAltCurrency); @@ -253,7 +259,20 @@ const WalletCardComponent: React.FC = ({ {t('Needs Backup')} ) : ( - {SupportedNetworkIcons} + + {SupportedNetworkIcons} + {tssMetadata ? ( + + ) : null} + {isMultisig ? ( + + ) : null} + )} {keyName} @@ -303,6 +322,8 @@ const WalletCardComponent: React.FC = ({ needsBackup, hideKeyBalance, pendingTssSession, + tssMetadata, + isMultisig, }} footer={CardFooter} onCTAPress={onPress} diff --git a/src/navigation/tabs/settings/external-services/screens/BanxaDetails.tsx b/src/navigation/tabs/settings/external-services/screens/BanxaDetails.tsx index 3967fa38d8..4cc35a159c 100644 --- a/src/navigation/tabs/settings/external-services/screens/BanxaDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/BanxaDetails.tsx @@ -15,9 +15,14 @@ import BanxaLogo from '../../../../../components/icons/external-services/banxa/b import { BanxaGetOrderDetailsRequestData, BanxaIncomingData, + BanxaOrderDetailsData, BanxaPaymentData, } from '../../../../../store/buy-crypto/buy-crypto.models'; -import {useAppDispatch, useLogger} from '../../../../../utils/hooks'; +import { + useAppDispatch, + useAppSelector, + useLogger, +} from '../../../../../utils/hooks'; import { showBottomNotificationModal, dismissBottomNotificationModal, @@ -50,13 +55,14 @@ import { BanxaStatus, banxaUrl, } from '../../../../services/buy-crypto/utils/banxa-utils'; -import {banxaGetOrderDetails} from '../../../../../store/buy-crypto/effects/banxa/banxa'; import {sleep} from '../../../../../utils/helper-methods'; import {SlateDark, White} from '../../../../../styles/colors'; import {Br} from '../../../../../components/styled/Containers'; import {openUrlWithInAppBrowser} from '../../../../../store/app/app.effects'; import {Link} from '../../../../../components/styled/Text'; import cloneDeep from 'lodash.clonedeep'; +import {RootState} from '../../../../../store'; +import {Key, Wallet} from '../../../../../store/wallet/wallet.models'; export interface BanxaDetailsProps { paymentRequest: BanxaPaymentData; @@ -76,6 +82,9 @@ const BanxaDetails: React.FC = () => { } = useRoute>(); const navigation = useNavigation(); const dispatch = useAppDispatch(); + const allKeys: {[key: string]: Key} = useAppSelector( + ({WALLET}: RootState) => WALLET.keys, + ); const [status, setStatus] = useState({ statusTitle: undefined, statusDescription: undefined, @@ -90,7 +99,7 @@ const BanxaDetails: React.FC = () => { setStatus(banxaGetStatusDetails(paymentRequest.status)); }; - const getOrderDetails = (force?: boolean) => { + const getOrderDetails = async (force?: boolean) => { if ( (['cancelled', 'complete', 'declined', 'expired', 'refunded'].includes( paymentRequest.status, @@ -101,166 +110,181 @@ const BanxaDetails: React.FC = () => { return; } + const walletIsSupported = (wallet: Wallet): boolean => + !!(wallet.credentials && wallet.isComplete()); + + const selectedWallet = Object.values(allKeys) + .filter(key => key.backupComplete) + .flatMap(key => key.wallets ?? []) + .find(walletIsSupported); + + if (!selectedWallet) { + logger.error('No supported wallet found for Banxa order details'); + return; + } + const requestData: BanxaGetOrderDetailsRequestData = { env: banxaEnv, order_id: paymentRequest.order_id, }; - banxaGetOrderDetails(requestData) - .then(data => { - if (!data?.data?.order) { - logger.error( - 'Banxa getOrderDetails Error: ' + data.message ?? 'No data', - ); - return; - } - const orderData = data.data.order; - let shouldUpdate = false; + try { + let data: BanxaOrderDetailsData | undefined; + const _data = await selectedWallet.banxaGetOrder(requestData); + data = _data?.body ?? _data; + if (!data?.data?.order) { + logger.error( + 'Banxa getOrderDetails Error: ' + (data?.message ?? 'No data'), + ); + return; + } + const orderData = data.data.order; + let shouldUpdate = false; + + if ( + orderData.coin_amount && + typeof orderData.coin_amount === 'number' && + orderData.coin_amount > 0 && + orderData.coin_amount != paymentRequest.crypto_amount + ) { + logger.debug( + `Updating crypto_amount from: ${paymentRequest.crypto_amount} to: ${orderData.coin_amount}`, + ); + paymentRequest.crypto_amount = orderData.coin_amount; + shouldUpdate = true; + } + if ( + orderData.fiat_amount && + typeof orderData.fiat_amount === 'number' && + orderData.fiat_amount > 0 && + orderData.fiat_amount != paymentRequest.fiat_total_amount + ) { + logger.debug( + `Updating fiat_total_amount from: ${paymentRequest.fiat_total_amount} to: ${orderData.fiat_amount}`, + ); + paymentRequest.fiat_total_amount = orderData.fiat_amount; + let fiatBaseAmount: number = cloneDeep(orderData.fiat_amount); if ( - orderData.coin_amount && - typeof orderData.coin_amount === 'number' && - orderData.coin_amount > 0 && - orderData.coin_amount != paymentRequest.crypto_amount - ) { - logger.debug( - `Updating crypto_amount from: ${paymentRequest.crypto_amount} to: ${orderData.coin_amount}`, - ); - paymentRequest.crypto_amount = orderData.coin_amount; - shouldUpdate = true; - } - if ( - orderData.fiat_amount && - typeof orderData.fiat_amount === 'number' && - orderData.fiat_amount > 0 && - orderData.fiat_amount != paymentRequest.fiat_total_amount - ) { - logger.debug( - `Updating fiat_total_amount from: ${paymentRequest.fiat_total_amount} to: ${orderData.fiat_amount}`, - ); - paymentRequest.fiat_total_amount = orderData.fiat_amount; - let fiatBaseAmount: number = cloneDeep(orderData.fiat_amount); - if ( - orderData.payment_fee && - typeof orderData.payment_fee === 'number' && - orderData.payment_fee >= 0 - ) { - fiatBaseAmount = orderData.fiat_amount - orderData.payment_fee; - paymentRequest.fiat_base_amount = fiatBaseAmount; - } - if ( - orderData.network_fee && - typeof orderData.network_fee === 'number' && - orderData.network_fee >= 0 - ) { - fiatBaseAmount = orderData.fiat_amount - orderData.network_fee; - paymentRequest.fiat_base_amount = fiatBaseAmount; - } - logger.debug( - `Updating fiat_base_amount from: ${paymentRequest.fiat_base_amount} to: ${fiatBaseAmount}`, - ); - shouldUpdate = true; - } - if ( - orderData.coin_code && - paymentRequest.coin !== orderData.coin_code.toUpperCase() - ) { - logger.debug( - `Updating coin from: ${ - paymentRequest.coin - } to: ${orderData.coin_code.toUpperCase()}`, - ); - paymentRequest.coin = orderData.coin_code.toUpperCase(); - shouldUpdate = true; - } - if ( - orderData.blockchain?.code && - paymentRequest.chain.toLowerCase() !== - orderData.blockchain?.code.toLowerCase() - ) { - logger.debug( - `Updating chain from: ${ - paymentRequest.chain - } to: ${orderData.blockchain.code.toLowerCase()}`, - ); - paymentRequest.chain = orderData.blockchain.code.toLowerCase(); - shouldUpdate = true; - } - if ( - orderData.fiat_code && - paymentRequest.fiat_total_amount_currency !== - orderData.fiat_code.toUpperCase() - ) { - logger.debug( - `Updating chain fiat_total_amount_currency: ${ - paymentRequest.fiat_total_amount_currency - } to: ${orderData.fiat_code.toUpperCase()}`, - ); - paymentRequest.fiat_total_amount_currency = - orderData.fiat_code.toUpperCase(); - shouldUpdate = true; - } - if ( - orderData.ref && - (!paymentRequest.ref || paymentRequest.ref !== orderData.ref) - ) { - logger.debug( - `Updating ref from: ${paymentRequest.ref} to: ${orderData.ref}`, - ); - paymentRequest.ref = orderData.ref; - shouldUpdate = true; - } - if ( - orderData.tx_hash && - (!paymentRequest.transaction_id || - paymentRequest.transaction_id !== orderData.tx_hash) + orderData.payment_fee && + typeof orderData.payment_fee === 'number' && + orderData.payment_fee >= 0 ) { - logger.debug( - `Updating transaction_id from: ${paymentRequest.transaction_id} to: ${orderData.tx_hash}`, - ); - paymentRequest.transaction_id = orderData.tx_hash; - shouldUpdate = true; + fiatBaseAmount = orderData.fiat_amount - orderData.payment_fee; + paymentRequest.fiat_base_amount = fiatBaseAmount; } if ( - orderData.status && - (!paymentRequest.status || paymentRequest.status !== orderData.status) + orderData.network_fee && + typeof orderData.network_fee === 'number' && + orderData.network_fee >= 0 ) { - logger.debug( - `Updating status from: ${paymentRequest.status} to: ${orderData.status}`, - ); - paymentRequest.status = orderData.status!; - updateStatusDescription(); - shouldUpdate = true; + fiatBaseAmount = orderData.fiat_amount - orderData.network_fee; + paymentRequest.fiat_base_amount = fiatBaseAmount; } + logger.debug( + `Updating fiat_base_amount from: ${paymentRequest.fiat_base_amount} to: ${fiatBaseAmount}`, + ); + shouldUpdate = true; + } + if ( + orderData.coin_code && + paymentRequest.coin !== orderData.coin_code.toUpperCase() + ) { + logger.debug( + `Updating coin from: ${ + paymentRequest.coin + } to: ${orderData.coin_code.toUpperCase()}`, + ); + paymentRequest.coin = orderData.coin_code.toUpperCase(); + shouldUpdate = true; + } + if ( + orderData.blockchain?.code && + paymentRequest.chain.toLowerCase() !== + orderData.blockchain?.code.toLowerCase() + ) { + logger.debug( + `Updating chain from: ${ + paymentRequest.chain + } to: ${orderData.blockchain.code.toLowerCase()}`, + ); + paymentRequest.chain = orderData.blockchain.code.toLowerCase(); + shouldUpdate = true; + } + if ( + orderData.fiat_code && + paymentRequest.fiat_total_amount_currency !== + orderData.fiat_code.toUpperCase() + ) { + logger.debug( + `Updating chain fiat_total_amount_currency: ${ + paymentRequest.fiat_total_amount_currency + } to: ${orderData.fiat_code.toUpperCase()}`, + ); + paymentRequest.fiat_total_amount_currency = + orderData.fiat_code.toUpperCase(); + shouldUpdate = true; + } + if ( + orderData.ref && + (!paymentRequest.ref || paymentRequest.ref !== orderData.ref) + ) { + logger.debug( + `Updating ref from: ${paymentRequest.ref} to: ${orderData.ref}`, + ); + paymentRequest.ref = orderData.ref; + shouldUpdate = true; + } + if ( + orderData.tx_hash && + (!paymentRequest.transaction_id || + paymentRequest.transaction_id !== orderData.tx_hash) + ) { + logger.debug( + `Updating transaction_id from: ${paymentRequest.transaction_id} to: ${orderData.tx_hash}`, + ); + paymentRequest.transaction_id = orderData.tx_hash; + shouldUpdate = true; + } + if ( + orderData.status && + (!paymentRequest.status || paymentRequest.status !== orderData.status) + ) { + logger.debug( + `Updating status from: ${paymentRequest.status} to: ${orderData.status}`, + ); + paymentRequest.status = orderData.status!; + updateStatusDescription(); + shouldUpdate = true; + } - if (shouldUpdate) { - const stateParams: BanxaIncomingData = { - banxaExternalId: paymentRequest.external_id, - banxaOrderId: paymentRequest.order_id, - status: paymentRequest.status, - cryptoAmount: paymentRequest.crypto_amount, - fiatTotalAmount: paymentRequest.fiat_total_amount, - fiatBaseAmount: paymentRequest.fiat_base_amount, - coin: paymentRequest.coin, - chain: paymentRequest.chain, - fiatTotalAmountCurrency: paymentRequest.fiat_total_amount_currency, - ref: paymentRequest.ref, - transactionId: paymentRequest.transaction_id, - }; - dispatch( - BuyCryptoActions.updatePaymentRequestBanxa({ - banxaIncomingData: stateParams, - }), - ); - - logger.debug( - 'Saved payment request with: ' + JSON.stringify(paymentRequest), - ); - } - }) - .catch(err => { - logger.error('Banxa getOrderDetails Error: ' + JSON.stringify(err)); - }); + if (shouldUpdate) { + const stateParams: BanxaIncomingData = { + banxaExternalId: paymentRequest.external_id, + banxaOrderId: paymentRequest.order_id, + status: paymentRequest.status, + cryptoAmount: paymentRequest.crypto_amount, + fiatTotalAmount: paymentRequest.fiat_total_amount, + fiatBaseAmount: paymentRequest.fiat_base_amount, + coin: paymentRequest.coin, + chain: paymentRequest.chain, + fiatTotalAmountCurrency: paymentRequest.fiat_total_amount_currency, + ref: paymentRequest.ref, + transactionId: paymentRequest.transaction_id, + }; + dispatch( + BuyCryptoActions.updatePaymentRequestBanxa({ + banxaIncomingData: stateParams, + }), + ); + + logger.debug( + 'Saved payment request with: ' + JSON.stringify(paymentRequest), + ); + } + } catch (err) { + logger.error('Banxa getOrderDetails Error: ' + JSON.stringify(err)); + } }; const onRefresh = async () => { diff --git a/src/navigation/tabs/settings/external-services/screens/ChangellyDetails.tsx b/src/navigation/tabs/settings/external-services/screens/ChangellyDetails.tsx index 2451bc65f0..7858694bb5 100644 --- a/src/navigation/tabs/settings/external-services/screens/ChangellyDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/ChangellyDetails.tsx @@ -9,9 +9,13 @@ import {Br} from '../../../../../components/styled/Containers'; import {SettingsComponent, SettingsContainer} from '../../SettingsRoot'; import haptic from '../../../../../components/haptic-feedback/haptic'; import {changellyTxData} from '../../../../../store/swap-crypto/swap-crypto.models'; -import {changellyGetStatus} from '../../../../../store/swap-crypto/effects/changelly/changelly'; + import ChangellyLogo from '../../../../../components/icons/external-services/changelly/changelly-logo'; -import {useAppDispatch, useLogger} from '../../../../../utils/hooks'; +import { + useAppDispatch, + useLogger, + useAppSelector, +} from '../../../../../utils/hooks'; import { showBottomNotificationModal, dismissBottomNotificationModal, @@ -21,7 +25,10 @@ import { changellyGetStatusDetails, Status, changellyGetStatusColor, + generateMessageId, } from '../../../../../navigation/services/swap-crypto/utils/changelly-utils'; +import {RootState} from '../../../../../store'; +import {Key, Wallet} from '../../../../../store/wallet/wallet.models'; import { RowDataContainer, CryptoAmountContainer, @@ -59,6 +66,9 @@ const ChangellyDetails: React.FC = () => { const logger = useLogger(); const theme = useTheme(); const dispatch = useAppDispatch(); + const allKeys: {[key: string]: Key} = useAppSelector( + ({WALLET}: RootState) => WALLET.keys, + ); const [status, setStatus] = useState({ statusTitle: undefined, statusDescription: undefined, @@ -83,33 +93,56 @@ const ChangellyDetails: React.FC = () => { setStatus(changellyGetStatusDetails(swapTx.status)); }; - const getStatus = (force?: boolean) => { + const getStatus = async (force?: boolean) => { if (swapTx.status === 'finished' && !force) { return; } - changellyGetStatus(swapTx.exchangeTxId, swapTx.status) - .then(data => { - if (data.error) { - logger.error('Changelly getStatus Error: ' + data.error.message); - return; - } - if (data.result != swapTx.status) { - logger.debug('Updating status to: ' + data.result); - swapTx.status = data.result; - updateStatusDescription(); - dispatch( - SwapCryptoActions.successTxChangelly({ - changellyTxData: swapTx, - }), - ); - - logger.debug('Saved swap with: ' + JSON.stringify(swapTx)); - } - }) - .catch(err => { - const errStr = err instanceof Error ? err.message : JSON.stringify(err); - logger.error('Changelly getStatus Error: ' + errStr); - }); + + const walletIsSupported = (wallet: Wallet): boolean => + !!(wallet.credentials && wallet.isComplete()); + + const selectedWallet = Object.values(allKeys) + .filter(key => key.backupComplete) + .flatMap(key => key.wallets ?? []) + .find(walletIsSupported); + + if (!selectedWallet) { + logger.error('No supported wallet found for Changelly status'); + return; + } + + try { + const reqData = { + id: generateMessageId(), + exchangeTxId: swapTx.exchangeTxId, + useV2: true, + }; + logger.debug( + 'Making a Changelly request with body: ' + JSON.stringify(reqData), + ); + const _raw = await selectedWallet.changellyGetStatus(reqData); + const data = _raw?.body ?? _raw; + + if (data.error) { + logger.error('Changelly getStatus Error: ' + data.error.message); + return; + } + if (data.result != swapTx.status) { + logger.debug('Updating status to: ' + data.result); + swapTx.status = data.result; + updateStatusDescription(); + dispatch( + SwapCryptoActions.successTxChangelly({ + changellyTxData: swapTx, + }), + ); + + logger.debug('Saved swap with: ' + JSON.stringify(swapTx)); + } + } catch (err) { + const errStr = err instanceof Error ? err.message : JSON.stringify(err); + logger.error('Changelly getStatus Error: ' + errStr); + } }; const onRefresh = async () => { diff --git a/src/navigation/tabs/settings/external-services/screens/MoonpayDetails.tsx b/src/navigation/tabs/settings/external-services/screens/MoonpayDetails.tsx index df3cd9030f..8982b203ae 100644 --- a/src/navigation/tabs/settings/external-services/screens/MoonpayDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/MoonpayDetails.tsx @@ -17,7 +17,11 @@ import { MoonpayPaymentData, MoonpayTransactionDetailsEmbeddedData, } from '../../../../../store/buy-crypto/buy-crypto.models'; -import {useAppDispatch, useLogger} from '../../../../../utils/hooks'; +import { + useAppDispatch, + useLogger, + useAppSelector, +} from '../../../../../utils/hooks'; import { showBottomNotificationModal, dismissBottomNotificationModal, @@ -45,10 +49,7 @@ import { import {useTranslation} from 'react-i18next'; import CopiedSvg from '../../../../../../assets/img/copied-success.svg'; import {BitpaySupportedCoins} from '../../../../../constants/currencies'; -import { - moonpayGetTransactionDetails, - moonpayGetTransactionDetailsEmbedded, -} from '../../../../../store/buy-crypto/effects/moonpay/moonpay'; +import {moonpayGetTransactionDetailsEmbedded} from '../../../../../store/buy-crypto/effects/moonpay/moonpay'; import { moonpayGetStatusColor, moonpayGetStatusDetails, @@ -61,6 +62,9 @@ import { getMoonpayEmbeddedCredentials, isMoonpayEmbeddedCredentialsValid, } from '../../../../../store/buy-crypto/buy-crypto.effects'; +import {moonpaySellEnv} from '../../../../../navigation/services/sell-crypto/utils/moonpay-sell-utils'; +import {RootState} from '../../../../../store'; +import {Key, Wallet} from '../../../../../store/wallet/wallet.models'; export interface MoonpayDetailsProps { paymentRequest: MoonpayPaymentData; } @@ -79,6 +83,9 @@ const MoonpayDetails: React.FC = () => { const logger = useLogger(); const theme = useTheme(); const dispatch = useAppDispatch(); + const allKeys: {[key: string]: Key} = useAppSelector( + ({WALLET}: RootState) => WALLET.keys, + ); const [status, setStatus] = useState({ statusTitle: undefined, statusDescription: undefined, @@ -182,12 +189,41 @@ const MoonpayDetails: React.FC = () => { } } else { try { - const data = await moonpayGetTransactionDetails( - paymentRequest.transaction_id, - paymentRequest.external_id, - ); + const walletIsSupported = (wallet: Wallet): boolean => + !!(wallet.credentials && wallet.isComplete()); + + const selectedWallet = Object.values(allKeys) + .filter(key => key.backupComplete) + .flatMap(key => key.wallets ?? []) + .find(walletIsSupported); + + if (!selectedWallet) { + logger.error( + 'No supported wallet found for Moonpay transaction details', + ); + return; + } + + let body; + if (paymentRequest.transaction_id) { + body = { + transactionId: paymentRequest.transaction_id, + env: moonpaySellEnv, + }; + } else if (paymentRequest.external_id) { + body = {externalId: paymentRequest.external_id, env: moonpaySellEnv}; + } else { + logger.debug('Moonpay getTransactionDetails: Missing parameters'); + return; + } + + const _data = await selectedWallet.moonpayGetTransactionDetails(body); + let data = _data?.body ?? _data; + if (Array.isArray(data)) { + data = data[0]; + } if (!data || data.type === 'NotFoundError') { - logger.error('Moonpay getTransactionDetails Error: ' + data.message); + logger.error('Moonpay getTransactionDetails Error: ' + data?.message); return; } if ( diff --git a/src/navigation/tabs/settings/external-services/screens/RampDetails.tsx b/src/navigation/tabs/settings/external-services/screens/RampDetails.tsx index 5ae01bcfd5..24a8b8a191 100644 --- a/src/navigation/tabs/settings/external-services/screens/RampDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/RampDetails.tsx @@ -161,7 +161,7 @@ const RampDetails: React.FC = () => { haptic('impactLight'); dispatch( openUrlWithInAppBrowser( - 'https://support.rampnetwork.com/en/articles/8647-how-do-i-check-the-status-of-my-crypto-purchase-on-ramp-network', + 'https://support.rampnetwork.com/en/articles/440-how-can-i-check-the-status-of-my-transaction', ), ); }}> @@ -188,7 +188,7 @@ const RampDetails: React.FC = () => { haptic('impactLight'); dispatch( openUrlWithInAppBrowser( - 'https://support.rampnetwork.com/en/articles/8647-how-do-i-check-the-status-of-my-crypto-purchase-on-ramp-network', + 'https://support.rampnetwork.com/en/articles/440-how-can-i-check-the-status-of-my-transaction', ), ); }}> diff --git a/src/navigation/tabs/settings/external-services/screens/SardineDetails.tsx b/src/navigation/tabs/settings/external-services/screens/SardineDetails.tsx index 8f75fbca53..3aca941370 100644 --- a/src/navigation/tabs/settings/external-services/screens/SardineDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/SardineDetails.tsx @@ -17,7 +17,11 @@ import { SardineIncomingData, SardinePaymentData, } from '../../../../../store/buy-crypto/buy-crypto.models'; -import {useAppDispatch, useLogger} from '../../../../../utils/hooks'; +import { + useAppDispatch, + useLogger, + useAppSelector, +} from '../../../../../utils/hooks'; import { showBottomNotificationModal, dismissBottomNotificationModal, @@ -49,12 +53,14 @@ import { sardineGetStatusDetails, SardineStatus, } from '../../../../services/buy-crypto/utils/sardine-utils'; -import {sardineGetOrderDetails} from '../../../../../store/buy-crypto/effects/sardine/sardine'; + import {sleep} from '../../../../../utils/helper-methods'; import {SlateDark, White} from '../../../../../styles/colors'; import {Br} from '../../../../../components/styled/Containers'; import {openUrlWithInAppBrowser} from '../../../../../store/app/app.effects'; import {Link} from '../../../../../components/styled/Text'; +import {RootState} from '../../../../../store'; +import {Key, Wallet} from '../../../../../store/wallet/wallet.models'; export interface SardineDetailsProps { paymentRequest: SardinePaymentData; @@ -74,6 +80,9 @@ const SardineDetails: React.FC = () => { } = useRoute>(); const navigation = useNavigation(); const dispatch = useAppDispatch(); + const allKeys: {[key: string]: Key} = useAppSelector( + ({WALLET}: RootState) => WALLET.keys, + ); const [status, setStatus] = useState({ statusTitle: undefined, statusDescription: undefined, @@ -88,7 +97,7 @@ const SardineDetails: React.FC = () => { setStatus(sardineGetStatusDetails(paymentRequest.status)); }; - const getOrderDetails = (force?: boolean) => { + const getOrderDetails = async (force?: boolean) => { if ( (['Complete', 'Declined', 'Expired'].includes(paymentRequest.status) || !paymentRequest.order_id) && @@ -97,71 +106,88 @@ const SardineDetails: React.FC = () => { return; } + const walletIsSupported = (wallet: Wallet): boolean => + !!(wallet.credentials && wallet.isComplete()); + + const selectedWallet = Object.values(allKeys) + .filter(key => key.backupComplete) + .flatMap(key => key.wallets ?? []) + .find(walletIsSupported); + + if (!selectedWallet) { + logger.error('No supported wallet found for Sardine order details'); + return; + } + const requestData: SardineGetOrderDetailsRequestData = { env: sardineEnv, orderId: paymentRequest.order_id, }; - sardineGetOrderDetails(requestData) - .then(data => { - if (!data) { - logger.error('Sardine getOrderDetails Error: ' + data.message); - return; - } - let shouldUpdate = false; - if ( - data.data?.quantity && - typeof data.data.quantity === 'number' && - data.data.quantity > 0 && - data.data.quantity != paymentRequest.crypto_amount - ) { - logger.debug( - `Updating crypto_amount from: ${paymentRequest.crypto_amount} to: ${data.data.quantity}`, - ); - paymentRequest.crypto_amount = data.data.quantity; - shouldUpdate = true; - } - if (!paymentRequest.order_id && data.id) { - paymentRequest.order_id = data.id; - shouldUpdate = true; - } - if (data.withdrawal?.txHash) { - paymentRequest.transaction_id = data.withdrawal.txHash; - shouldUpdate = true; - } - if ( - data.status && - (!paymentRequest.status || data.status != paymentRequest.status) - ) { - logger.debug( - `Updating status from: ${paymentRequest.status} to: ${data.status}`, - ); - paymentRequest.status = data.status; - updateStatusDescription(); - shouldUpdate = true; - } - - if (shouldUpdate) { - const stateParams: SardineIncomingData = { - sardineExternalId: paymentRequest.external_id, - status: paymentRequest.status, - cryptoAmount: paymentRequest.crypto_amount, - transactionId: paymentRequest.transaction_id, - }; - dispatch( - BuyCryptoActions.updatePaymentRequestSardine({ - sardineIncomingData: stateParams, - }), - ); - - logger.debug( - 'Saved payment request with: ' + JSON.stringify(paymentRequest), - ); - } - }) - .catch(err => { - logger.error('Sardine getOrderDetails Error: ' + JSON.stringify(err)); - }); + try { + const _rawData = await selectedWallet.sardineGetOrdersDetails( + requestData, + ); + let rawData = _rawData?.body ?? _rawData; + const data = Array.isArray(rawData) ? rawData[0] : rawData; + + if (!data) { + logger.error('Sardine getOrderDetails Error: No data'); + return; + } + let shouldUpdate = false; + if ( + data.data?.quantity && + typeof data.data.quantity === 'number' && + data.data.quantity > 0 && + data.data.quantity != paymentRequest.crypto_amount + ) { + logger.debug( + `Updating crypto_amount from: ${paymentRequest.crypto_amount} to: ${data.data.quantity}`, + ); + paymentRequest.crypto_amount = data.data.quantity; + shouldUpdate = true; + } + if (!paymentRequest.order_id && data.id) { + paymentRequest.order_id = data.id; + shouldUpdate = true; + } + if (data.withdrawal?.txHash) { + paymentRequest.transaction_id = data.withdrawal.txHash; + shouldUpdate = true; + } + if ( + data.status && + (!paymentRequest.status || data.status != paymentRequest.status) + ) { + logger.debug( + `Updating status from: ${paymentRequest.status} to: ${data.status}`, + ); + paymentRequest.status = data.status; + updateStatusDescription(); + shouldUpdate = true; + } + + if (shouldUpdate) { + const stateParams: SardineIncomingData = { + sardineExternalId: paymentRequest.external_id, + status: paymentRequest.status, + cryptoAmount: paymentRequest.crypto_amount, + transactionId: paymentRequest.transaction_id, + }; + dispatch( + BuyCryptoActions.updatePaymentRequestSardine({ + sardineIncomingData: stateParams, + }), + ); + + logger.debug( + 'Saved payment request with: ' + JSON.stringify(paymentRequest), + ); + } + } catch (err) { + logger.error('Sardine getOrderDetails Error: ' + JSON.stringify(err)); + } }; const onRefresh = async () => { diff --git a/src/navigation/tabs/settings/external-services/screens/ThorswapDetails.tsx b/src/navigation/tabs/settings/external-services/screens/ThorswapDetails.tsx index 8100546069..bc2399238f 100644 --- a/src/navigation/tabs/settings/external-services/screens/ThorswapDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/ThorswapDetails.tsx @@ -11,7 +11,11 @@ import haptic from '../../../../../components/haptic-feedback/haptic'; import {thorswapTxData} from '../../../../../store/swap-crypto/swap-crypto.models'; // import {thorswapGetStatus} from '../../../../../store/swap-crypto/effects/thorswap/thorswap'; import ThorswapLogo from '../../../../../components/icons/external-services/thorswap/thorswap-logo'; -import {useAppDispatch, useLogger} from '../../../../../utils/hooks'; +import { + useAppDispatch, + useLogger, + useAppSelector, +} from '../../../../../utils/hooks'; import { showBottomNotificationModal, dismissBottomNotificationModal, @@ -51,8 +55,9 @@ import { ThorswapGetSwapTxRequestData, ThorswapTrackingStatus, } from '../../../../../store/swap-crypto/models/thorswap.models'; -import {thorswapGetSwapTx} from '../../../../../store/swap-crypto/effects/thorswap/thorswap'; import cloneDeep from 'lodash.clonedeep'; +import {RootState} from '../../../../../store'; +import {Key, Wallet} from '../../../../../store/wallet/wallet.models'; export interface ThorswapDetailsProps { swapTx: thorswapTxData; @@ -67,6 +72,9 @@ const ThorswapDetails: React.FC = () => { const logger = useLogger(); const theme = useTheme(); const dispatch = useAppDispatch(); + const allKeys: {[key: string]: Key} = useAppSelector( + ({WALLET}: RootState) => WALLET.keys, + ); const [status, setStatus] = useState({ statusTitle: undefined, statusDescription: undefined, @@ -126,10 +134,22 @@ const ThorswapDetails: React.FC = () => { hash: swapTx.txHash, }; + const walletIsSupported = (wallet: Wallet): boolean => + !!(wallet.credentials && wallet.isComplete()); + + const selectedWallet = Object.values(allKeys) + .filter(key => key.backupComplete) + .flatMap(key => key.wallets ?? []) + .find(walletIsSupported); + + if (!selectedWallet) { + logger.error('No supported wallet found for Thorswap status'); + return; + } + try { - const swapTxData: ThorswapGetSwapTxData = await thorswapGetSwapTx( - reqData, - ); + const _raw = await selectedWallet.thorswapGetSwapTx(reqData); + const swapTxData: ThorswapGetSwapTxData = _raw?.body ?? _raw; let shouldUpdate = false; if (swapTxData?.result) { if ( diff --git a/src/navigation/tabs/settings/external-services/screens/TransakDetails.tsx b/src/navigation/tabs/settings/external-services/screens/TransakDetails.tsx index 80f2dcd2ab..e9335146ae 100644 --- a/src/navigation/tabs/settings/external-services/screens/TransakDetails.tsx +++ b/src/navigation/tabs/settings/external-services/screens/TransakDetails.tsx @@ -54,7 +54,6 @@ import { transakGetStatusDetails, TransakStatus, } from '../../../../services/buy-crypto/utils/transak-utils'; -import {transakGetOrderDetails} from '../../../../../store/buy-crypto/effects/transak/transak'; import {sleep} from '../../../../../utils/helper-methods'; import {SlateDark, White} from '../../../../../styles/colors'; import {Br} from '../../../../../components/styled/Containers'; @@ -117,6 +116,19 @@ const TransakDetails: React.FC = () => { return; } + const walletIsSupported = (wallet: Wallet): boolean => + !!(wallet.credentials && wallet.isComplete()); + + const selectedWallet = Object.values(allKeys) + .filter(key => key.backupComplete) + .flatMap(key => key.wallets ?? []) + .find(walletIsSupported); + + if (!selectedWallet) { + logger.error('No supported wallet found for Transak order details'); + return; + } + const nowTimestamp = (Date.now() / 1000) | 0; let _accessToken = accessTokenTransak?.accessToken; let _expiresAt = accessTokenTransak?.expiresAt; @@ -127,60 +139,28 @@ const TransakDetails: React.FC = () => { 'Transak access token expired. Fetching new one from TransakDetails...', ); } - const keysList: Key[] = Object.values(allKeys).filter( - key => key.backupComplete, - ); - - if (keysList[0]) { - const walletIsSupported = (wallet: Wallet): boolean => { - return wallet.credentials && wallet.isComplete(); - }; - - const keyHasSupportedWallets = (wallets: Wallet[]): boolean => { - const supportedWallets = wallets.filter(wallet => - walletIsSupported(wallet), - ); - return !!supportedWallets[0]; - }; - - const availableKeys = keysList.filter(key => { - return key.wallets && keyHasSupportedWallets(key.wallets); - }); + const _tokenData = await selectedWallet.transakGetAccessToken({ + env: transakEnv, + }); + const tokenData: TransakAccessTokenData | undefined = + _tokenData?.body?.data ?? _tokenData?.body ?? _tokenData; - if (availableKeys[0]) { - const firstKey = availableKeys[0]; - - const firstKeyAllWallets: Wallet[] = firstKey.wallets; - let allowedWallets = firstKeyAllWallets.filter(wallet => - walletIsSupported(wallet), - ); - - const selectedWallet = allowedWallets[0]; - - let data: TransakAccessTokenData | undefined; - const _data = await selectedWallet.transakGetAccessToken({ + if (tokenData?.accessToken) { + logger.debug( + 'Transak access token fetched successfully from TransakDetails.', + ); + dispatch( + BuyCryptoActions.updateAccessTokenTransak({ env: transakEnv, - }); - data = _data?.body?.data ?? _data?.body ?? _data; - - if (data?.accessToken) { - logger.debug( - 'Transak access token fetched successfully from TransakDetails.', - ); - dispatch( - BuyCryptoActions.updateAccessTokenTransak({ - env: transakEnv, - ...data, - }), - ); - _accessToken = data.accessToken; - } else { - const err = - 'Error fetching Transak access token in TransakDetails. Error: No accessToken provided'; - logger.error(`${err}`); - // Continue anyway, as we can generate a new access token in the BWS if needed. - } - } + ...tokenData, + }), + ); + _accessToken = tokenData.accessToken; + } else { + const err = + 'Error fetching Transak access token in TransakDetails. Error: No accessToken provided'; + logger.error(`${err}`); + // Continue anyway, as we can generate a new access token in the BWS if needed. } } catch (err: any) { let msg: string = @@ -203,141 +183,140 @@ const TransakDetails: React.FC = () => { accessToken: _accessToken, }; - transakGetOrderDetails(requestData) - .then(data => { - if (!data?.data) { - logger.error('Transak getOrderDetails Error: No data'); - return; - } - const orderData = data.data; - let shouldUpdate = false; - if ( - orderData.cryptoAmount && - typeof orderData.cryptoAmount === 'number' && - orderData.cryptoAmount > 0 && - orderData.cryptoAmount != paymentRequest.crypto_amount - ) { - logger.debug( - `Updating crypto_amount from: ${paymentRequest.crypto_amount} to: ${orderData.cryptoAmount}`, - ); - paymentRequest.crypto_amount = orderData.cryptoAmount; - shouldUpdate = true; - } - if ( - orderData.fiatAmount && - typeof orderData.fiatAmount === 'number' && - orderData.fiatAmount > 0 && - orderData.fiatAmount != paymentRequest.fiat_total_amount - ) { - logger.debug( - `Updating fiat_total_amount from: ${paymentRequest.fiat_total_amount} to: ${orderData.fiatAmount}`, - ); - paymentRequest.fiat_total_amount = orderData.fiatAmount; - let fiatBaseAmount: number = cloneDeep( - paymentRequest.fiat_base_amount, - ); - if ( - orderData.totalFeeInFiat && - typeof orderData.totalFeeInFiat === 'number' && - orderData.totalFeeInFiat > 0 - ) { - fiatBaseAmount = - Number(orderData.fiatAmount) - Number(orderData.totalFeeInFiat); - logger.debug( - `Updating fiat_base_amount from: ${paymentRequest.fiat_base_amount} to: ${fiatBaseAmount}`, - ); - paymentRequest.fiat_base_amount = fiatBaseAmount; - } - shouldUpdate = true; - } - if ( - orderData.fiatCurrency && - paymentRequest.fiat_total_amount_currency !== - orderData.fiatCurrency.toUpperCase() - ) { - logger.debug( - `Updating fiat_total_amount_currency from: ${ - paymentRequest.fiat_total_amount_currency - } to: ${orderData.fiatCurrency.toUpperCase()}`, - ); - paymentRequest.fiat_total_amount_currency = - orderData.fiatCurrency.toUpperCase(); - shouldUpdate = true; - } - if ( - orderData.cryptoCurrency && - paymentRequest.coin !== orderData.cryptoCurrency.toUpperCase() - ) { - logger.debug( - `Updating coin from: ${ - paymentRequest.coin - } to: ${orderData.cryptoCurrency.toUpperCase()}`, - ); - paymentRequest.coin = cloneDeep( - orderData.cryptoCurrency, - ).toUpperCase(); - if ( - orderData.network && - orderData.network == 'mainnet' && - paymentRequest.chain !== orderData.cryptoCurrency.toLowerCase() - ) { - logger.debug( - `Updating chain from: ${ - paymentRequest.chain - } to: ${orderData.cryptoCurrency.toLowerCase()}`, - ); - paymentRequest.coin = orderData.network; - } - shouldUpdate = true; - } + try { + const _orderData = await selectedWallet.transakGetOrderDetails( + requestData, + ); + const data = _orderData?.body ?? _orderData; + + if (!data?.data) { + logger.error('Transak getOrderDetails Error: No data'); + return; + } + const orderData = data.data; + let shouldUpdate = false; + if ( + orderData.cryptoAmount && + typeof orderData.cryptoAmount === 'number' && + orderData.cryptoAmount > 0 && + orderData.cryptoAmount != paymentRequest.crypto_amount + ) { + logger.debug( + `Updating crypto_amount from: ${paymentRequest.crypto_amount} to: ${orderData.cryptoAmount}`, + ); + paymentRequest.crypto_amount = orderData.cryptoAmount; + shouldUpdate = true; + } + if ( + orderData.fiatAmount && + typeof orderData.fiatAmount === 'number' && + orderData.fiatAmount > 0 && + orderData.fiatAmount != paymentRequest.fiat_total_amount + ) { + logger.debug( + `Updating fiat_total_amount from: ${paymentRequest.fiat_total_amount} to: ${orderData.fiatAmount}`, + ); + paymentRequest.fiat_total_amount = orderData.fiatAmount; + let fiatBaseAmount: number = cloneDeep(paymentRequest.fiat_base_amount); if ( - orderData.transactionHash && - paymentRequest.transaction_id !== orderData.transactionHash + orderData.totalFeeInFiat && + typeof orderData.totalFeeInFiat === 'number' && + orderData.totalFeeInFiat > 0 ) { + fiatBaseAmount = + Number(orderData.fiatAmount) - Number(orderData.totalFeeInFiat); logger.debug( - `Updating transactionHash from: ${paymentRequest.transaction_id} to: ${orderData.transactionHash}`, + `Updating fiat_base_amount from: ${paymentRequest.fiat_base_amount} to: ${fiatBaseAmount}`, ); - paymentRequest.transaction_id = orderData.transactionHash; - shouldUpdate = true; + paymentRequest.fiat_base_amount = fiatBaseAmount; } + shouldUpdate = true; + } + if ( + orderData.fiatCurrency && + paymentRequest.fiat_total_amount_currency !== + orderData.fiatCurrency.toUpperCase() + ) { + logger.debug( + `Updating fiat_total_amount_currency from: ${ + paymentRequest.fiat_total_amount_currency + } to: ${orderData.fiatCurrency.toUpperCase()}`, + ); + paymentRequest.fiat_total_amount_currency = + orderData.fiatCurrency.toUpperCase(); + shouldUpdate = true; + } + if ( + orderData.cryptoCurrency && + paymentRequest.coin !== orderData.cryptoCurrency.toUpperCase() + ) { + logger.debug( + `Updating coin from: ${ + paymentRequest.coin + } to: ${orderData.cryptoCurrency.toUpperCase()}`, + ); + paymentRequest.coin = cloneDeep(orderData.cryptoCurrency).toUpperCase(); if ( - orderData.status && - (!paymentRequest.status || orderData.status != paymentRequest.status) + orderData.network && + orderData.network == 'mainnet' && + paymentRequest.chain !== orderData.cryptoCurrency.toLowerCase() ) { logger.debug( - `Updating status from: ${paymentRequest.status} to: ${orderData.status}`, + `Updating chain from: ${ + paymentRequest.chain + } to: ${orderData.cryptoCurrency.toLowerCase()}`, ); - paymentRequest.status = orderData.status; - updateStatusDescription(); - shouldUpdate = true; + paymentRequest.coin = orderData.network; } + shouldUpdate = true; + } + if ( + orderData.transactionHash && + paymentRequest.transaction_id !== orderData.transactionHash + ) { + logger.debug( + `Updating transactionHash from: ${paymentRequest.transaction_id} to: ${orderData.transactionHash}`, + ); + paymentRequest.transaction_id = orderData.transactionHash; + shouldUpdate = true; + } + if ( + orderData.status && + (!paymentRequest.status || orderData.status != paymentRequest.status) + ) { + logger.debug( + `Updating status from: ${paymentRequest.status} to: ${orderData.status}`, + ); + paymentRequest.status = orderData.status; + updateStatusDescription(); + shouldUpdate = true; + } - if (shouldUpdate) { - const stateParams: TransakIncomingData = { - transakExternalId: paymentRequest.external_id, - status: paymentRequest.status, - cryptoAmount: paymentRequest.crypto_amount, - transactionId: paymentRequest.transaction_id, - fiatTotalAmount: paymentRequest.fiat_total_amount, - fiatBaseAmount: paymentRequest.fiat_base_amount, - coin: paymentRequest.coin, - chain: paymentRequest.chain, - fiatTotalAmountCurrency: paymentRequest.fiat_total_amount_currency, - }; - dispatch( - BuyCryptoActions.updatePaymentRequestTransak({ - transakIncomingData: stateParams, - }), - ); + if (shouldUpdate) { + const stateParams: TransakIncomingData = { + transakExternalId: paymentRequest.external_id, + status: paymentRequest.status, + cryptoAmount: paymentRequest.crypto_amount, + transactionId: paymentRequest.transaction_id, + fiatTotalAmount: paymentRequest.fiat_total_amount, + fiatBaseAmount: paymentRequest.fiat_base_amount, + coin: paymentRequest.coin, + chain: paymentRequest.chain, + fiatTotalAmountCurrency: paymentRequest.fiat_total_amount_currency, + }; + dispatch( + BuyCryptoActions.updatePaymentRequestTransak({ + transakIncomingData: stateParams, + }), + ); - logger.debug( - 'Saved payment request with: ' + JSON.stringify(paymentRequest), - ); - } - }) - .catch(err => { - logger.error('Transak getOrderDetails Error: ' + JSON.stringify(err)); - }); + logger.debug( + 'Saved payment request with: ' + JSON.stringify(paymentRequest), + ); + } + } catch (err) { + logger.error('Transak getOrderDetails Error: ' + JSON.stringify(err)); + } }; const onRefresh = async () => { diff --git a/src/navigation/wallet/WalletGroup.tsx b/src/navigation/wallet/WalletGroup.tsx index 9033fc2b93..baef7c7161 100644 --- a/src/navigation/wallet/WalletGroup.tsx +++ b/src/navigation/wallet/WalletGroup.tsx @@ -299,6 +299,7 @@ const WalletGroup = ({Wallet, theme}: WalletProps) => { /> null, }} name={WalletScreens.BACKUP_SHARED_KEY} @@ -424,6 +425,8 @@ const WalletGroup = ({Wallet, theme}: WalletProps) => { /> null, headerTitle: () => ( {t('Invite Co-Signers')} ), diff --git a/src/navigation/wallet/components/OptionsSheet.tsx b/src/navigation/wallet/components/OptionsSheet.tsx index 8caf0a0d4e..03995439eb 100644 --- a/src/navigation/wallet/components/OptionsSheet.tsx +++ b/src/navigation/wallet/components/OptionsSheet.tsx @@ -1,7 +1,8 @@ import React, {ReactElement} from 'react'; +import {useTheme} from '@react-navigation/native'; import SheetModal from '../../../components/modal/base/sheet/SheetModal'; import {BaseText, H4, TextAlign} from '../../../components/styled/Text'; -import styled from 'styled-components/native'; +import styled, {css} from 'styled-components/native'; import { ActiveOpacity, SheetContainer, @@ -11,23 +12,62 @@ import {Platform, Image, ImageSourcePropType} from 'react-native'; import { Black, Slate, + Slate30, White, SlateDark, - Warning, Caution, + Feather, + LightBlack, + Warning, + Warning25, } from '../../../styles/colors'; import {sleep} from '../../../utils/helper-methods'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; +import Back from '../../../components/back/Back'; import AngleRight from '../../../../assets/img/angle-right.svg'; +import ClockOutlineIcon from '../../../../assets/img/icon-clock-outline.svg'; +import AlertTriangleIcon from '../../../../assets/img/icon-alert-triangle.svg'; +import InfoIcon from '../../../components/icons/info/Info'; + +const OptionsHeaderContainer = styled.View<{cardStyle?: boolean}>` + flex-direction: row; + align-items: center; + justify-content: space-between; + position: relative; + margin-bottom: ${({cardStyle}) => (cardStyle ? 12 : 25)}px; +`; -const OptionsTitleContainer = styled.View` - margin-bottom: 25px; +const OptionsHeaderTitleContainer = styled.View` + position: absolute; + left: 0; + right: 0; + align-items: center; `; -const OptionContainer = styled(TouchableOpacity)` +const OptionsHeaderPlaceholder = styled.View` + width: 41px; +`; + +const OptionContainer = styled(TouchableOpacity)< + SheetParams & {cardStyle?: boolean} +>` flex-direction: row; align-items: stretch; - padding-${({placement}) => placement}: 31px; + ${({cardStyle, placement}) => + cardStyle + ? css` + padding: 16px; + margin-bottom: 12px; + ` + : css` + padding-${placement}: 31px; + `} + ${({cardStyle, theme: {dark}}) => + cardStyle && + css` + background-color: ${dark ? LightBlack : Feather}; + border-radius: 12px; + `} `; const OptionIconContainer = styled.View` @@ -35,37 +75,53 @@ const OptionIconContainer = styled.View` width: 20px; `; -const OptionTextContainer = styled.View` +const OptionTextContainer = styled.View<{cardStyle?: boolean}>` align-items: flex-start; - justify-content: space-around; + justify-content: center; flex-direction: column; - margin: 0 20px; + margin: ${({cardStyle}) => (cardStyle ? '0 8px 0 0' : '0 20px')}; flex: 1; `; -const OptionTitleText = styled(BaseText)` +const OptionTitleText = styled(BaseText)<{cardStyle?: boolean}>` font-style: normal; - font-weight: 600; + font-weight: ${({cardStyle}) => (cardStyle ? 500 : 600)}; font-size: 16px; - line-height: 22px; + line-height: ${({cardStyle}) => (cardStyle ? 24 : 22)}px; color: ${({theme: {dark}}) => (dark ? White : Black)}; margin-bottom: 6px; `; -const OptionDescriptionText = styled(BaseText)` +const OptionDescriptionText = styled(BaseText)<{cardStyle?: boolean}>` font-style: normal; font-weight: 400; - font-size: 14px; - line-height: 20px; - color: ${({theme: {dark}}) => (dark ? Slate : Black)}; + font-size: ${({cardStyle}) => (cardStyle ? 16 : 14)}px; + line-height: ${({cardStyle}) => (cardStyle ? 24 : 20)}px; + color: ${({cardStyle, theme: {dark}}) => + cardStyle ? (dark ? Slate30 : SlateDark) : dark ? Slate : Black}; margin-top: 3px; `; const SubDescriptionContainer = styled.View` - background-color: ${({theme: {dark}}) => (dark ? '#000000' : '#F5F5F5')}; - border-radius: 8px; - padding: 12px 14px; - margin-top: 12px; + width: 100%; + background-color: ${({theme: {dark}}) => (dark ? Black : White)}; + border-radius: 12px; + padding: 16px; + margin-top: 8px; +`; + +const SubDescriptionRow = styled.View` + width: 100%; + flex-direction: row; + align-items: center; +`; + +const SubDescriptionRowSpacer = styled.View` + height: 4px; +`; + +const SubDescriptionIconContainer = styled.View` + margin-right: 4px; `; const OptionSubDescriptionText = styled(BaseText)` @@ -74,17 +130,44 @@ const OptionSubDescriptionText = styled(BaseText)` font-size: 13px; line-height: 20px; color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; + flex: 1; `; -const BoldText = styled(BaseText)` - font-weight: 700; - color: ${({theme: {dark}}) => (dark ? White : SlateDark)}; +const OptionBadgeContainer = styled.View` + flex-direction: row; + align-items: center; + align-self: flex-start; + background-color: ${Warning25}; + border-radius: 4px; + padding: 4px 8px; + margin-bottom: 8px; +`; + +const OptionBadgeText = styled(BaseText)` + font-style: normal; + font-weight: 400; + font-size: 13px; + line-height: 20px; + color: ${Warning}; `; +export interface SubDescriptionItem { + icon: 'clock' | 'warning' | 'info'; + text: string; +} + +const subDescriptionIcons = { + clock: ClockOutlineIcon, + warning: AlertTriangleIcon, + info: InfoIcon, +}; + export interface Option { img?: ReactElement; imgSrc?: ImageSourcePropType; - subDescription?: string; + badge?: string; + subDescriptionItems?: SubDescriptionItem[]; + cardStyle?: boolean; title?: string; description?: string; onPress: () => void; @@ -98,29 +181,22 @@ interface Props extends SheetParams { isVisible: boolean; closeModal: () => void; title?: string; + onBack?: () => void; options: Array