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