diff --git a/VERSION_CODE b/VERSION_CODE
index 4c009fb2f..bea0d09c4 100644
--- a/VERSION_CODE
+++ b/VERSION_CODE
@@ -1 +1 @@
-206
\ No newline at end of file
+207
\ No newline at end of file
diff --git a/app/Navigation.tsx b/app/Navigation.tsx
index b28090dd0..56f5e9ddf 100644
--- a/app/Navigation.tsx
+++ b/app/Navigation.tsx
@@ -96,6 +96,8 @@ import { PendingTxsWatcher } from './components/PendingTxsWatcher';
import { TonconnectWatcher } from './components/TonconnectWatcher';
import { SessionWatcher } from './components/SessionWatcher';
import { MandatoryAuthSetupFragment } from './fragments/secure/MandatoryAuthSetupFragment';
+import { WebViewPreloader } from './components/WebViewPreloader';
+import { holdersUrl } from './engine/api/holders/fetchUserState';
const Stack = createNativeStackNavigator();
Stack.Navigator.displayName = 'MainStack';
@@ -469,6 +471,7 @@ export const Navigation = memo(() => {
+
);
diff --git a/app/analytics/mixpanel.ts b/app/analytics/mixpanel.ts
index c4726c19e..238d6c9a4 100644
--- a/app/analytics/mixpanel.ts
+++ b/app/analytics/mixpanel.ts
@@ -13,6 +13,8 @@ export enum MixpanelEvent {
HoldersEnrollment = 'holders_entrollment',
HoldersInfo = 'holders_info',
HoldersInfoClose = 'holders_info_close',
+ HoldersLoadingTime = 'holders_loading_time',
+ holdersLongLoadingTime = 'holders_long_loading_time',
HoldersEnrollmentClose = 'holders_entrollment_close',
HoldersClose = 'holders_close',
Connect = 'connect',
diff --git a/app/components/ScreenHeader.tsx b/app/components/ScreenHeader.tsx
index 926c76ca1..504cb4872 100644
--- a/app/components/ScreenHeader.tsx
+++ b/app/components/ScreenHeader.tsx
@@ -128,7 +128,7 @@ export const ScreenHeader = memo((
Promise<{ uri: string }>
}) => {
const theme = useTheme();
+ const toaster = useToaster();
const onShare = useCallback(async () => {
let screenShot: { uri: string } | undefined;
if (onScreenCapture) {
screenShot = await onScreenCapture();
}
- Share.open({
- title: t('receive.share.title'),
- message: body,
- url: screenShot?.uri,
- });
+ try {
+ await Share.open({
+ title: t('receive.share.title'),
+ message: body,
+ url: screenShot?.uri,
+ });
+ } catch {
+ toaster.show({
+ type: 'error',
+ message: t('receive.share.error')
+ });
+ }
}, [body]);
return (
diff --git a/app/components/WebViewPreloader.tsx b/app/components/WebViewPreloader.tsx
new file mode 100644
index 000000000..11375e73a
--- /dev/null
+++ b/app/components/WebViewPreloader.tsx
@@ -0,0 +1,14 @@
+import { memo } from "react";
+import { View } from "react-native";
+import WebView from "react-native-webview";
+
+export const WebViewPreloader = memo(({ url }: { url: string }) => {
+ return (
+
+
+
+ );
+});
\ No newline at end of file
diff --git a/app/components/browser/BrowserExtensions.tsx b/app/components/browser/BrowserExtensions.tsx
index 2f485239a..9fb3eedc0 100644
--- a/app/components/browser/BrowserExtensions.tsx
+++ b/app/components/browser/BrowserExtensions.tsx
@@ -11,7 +11,7 @@ import { extractDomain } from "../../engine/utils/extractDomain";
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
import { useDimensions } from "@react-native-community/hooks";
import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { holdersUrl as resolveHoldersUrl } from '../../engine/api/holders/fetchAccountState';
+import { holdersUrl as resolveHoldersUrl } from '../../engine/api/holders/fetchUserState';
import { Typography } from "../styles";
import { ConnectedApp } from "../../engine/hooks/dapps/useTonConnectExtenstions";
diff --git a/app/components/products/HoldersAccountItem.tsx b/app/components/products/HoldersAccountItem.tsx
index 6b1005d2a..aa5dcb5b3 100644
--- a/app/components/products/HoldersAccountItem.tsx
+++ b/app/components/products/HoldersAccountItem.tsx
@@ -7,7 +7,7 @@ import { useTypedNavigation } from "../../utils/useTypedNavigation";
import Animated from "react-native-reanimated";
import { useAnimatedPressedInOut } from "../../utils/useAnimatedPressedInOut";
import { useIsConnectAppReady, useJettonContent, usePrice, useTheme } from "../../engine/hooks";
-import { HoldersAccountState, holdersUrl } from "../../engine/api/holders/fetchAccountState";
+import { HoldersUserState, holdersUrl } from "../../engine/api/holders/fetchUserState";
import { GeneralHoldersAccount, GeneralHoldersCard } from "../../engine/api/holders/fetchAccounts";
import { PerfText } from "../basic/PerfText";
import { Typography } from "../styles";
@@ -18,7 +18,7 @@ import { HoldersAccountStatus } from "../../engine/hooks/holders/useHoldersAccou
import { WImage } from "../WImage";
import { toBnWithDecimals } from "../../utils/withDecimals";
import { toNano } from "@ton/core";
-import { HoldersAppParams } from "../../fragments/holders/HoldersAppFragment";
+import { HoldersAppParams, HoldersAppParamsType } from "../../fragments/holders/HoldersAppFragment";
import { getAccountName } from "../../utils/holders/getAccountName";
import IcTonIcon from '@assets/ic-ton-acc.svg';
@@ -70,7 +70,7 @@ export const HoldersAccountItem = memo((props: {
return true;
}
- if (holdersAccStatus.state === HoldersAccountState.NeedEnrollment) {
+ if (holdersAccStatus.state === HoldersUserState.NeedEnrollment) {
return true;
}
@@ -86,12 +86,12 @@ export const HoldersAccountItem = memo((props: {
props.onBeforeOpen?.();
if (needsEnrollment) {
- const onEnrollType: HoldersAppParams = { type: 'account', id: props.account.id };
+ const onEnrollType: HoldersAppParams = { type: HoldersAppParamsType.Account, id: props.account.id };
navigation.navigateHoldersLanding({ endpoint: url, onEnrollType }, props.isTestnet);
return;
}
- navigation.navigateHolders({ type: 'account', id: props.account.id }, props.isTestnet);
+ navigation.navigateHolders({ type: HoldersAppParamsType.Account, id: props.account.id }, props.isTestnet);
}, [props.account, needsEnrollment, props.isTestnet]);
const { onPressIn, onPressOut, animatedStyle } = useAnimatedPressedInOut();
diff --git a/app/components/products/HoldersPrepaidCard.tsx b/app/components/products/HoldersPrepaidCard.tsx
index 90461f302..ac919670e 100644
--- a/app/components/products/HoldersPrepaidCard.tsx
+++ b/app/components/products/HoldersPrepaidCard.tsx
@@ -6,7 +6,7 @@ import { useTypedNavigation } from "../../utils/useTypedNavigation";
import Animated from "react-native-reanimated";
import { useAnimatedPressedInOut } from "../../utils/useAnimatedPressedInOut";
import { useIsConnectAppReady, useTheme } from "../../engine/hooks";
-import { HoldersAccountState, holdersUrl } from "../../engine/api/holders/fetchAccountState";
+import { HoldersUserState, holdersUrl } from "../../engine/api/holders/fetchUserState";
import { GeneralHoldersCard, PrePaidHoldersCard } from "../../engine/api/holders/fetchAccounts";
import { PerfText } from "../basic/PerfText";
import { Typography } from "../styles";
@@ -15,7 +15,7 @@ import { toNano } from "@ton/core";
import { CurrencySymbols } from "../../utils/formatCurrency";
import { HoldersAccountCard } from "./HoldersAccountCard";
import { HoldersAccountStatus } from "../../engine/hooks/holders/useHoldersAccountStatus";
-import { HoldersAppParams } from "../../fragments/holders/HoldersAppFragment";
+import { HoldersAppParams, HoldersAppParamsType } from "../../fragments/holders/HoldersAppFragment";
import { useLockAppWithAuthState } from "../../engine/hooks/settings";
export const HoldersPrepaidCard = memo((props: {
@@ -50,7 +50,7 @@ export const HoldersPrepaidCard = memo((props: {
return true;
}
- if (holdersAccStatus.state === HoldersAccountState.NeedEnrollment) {
+ if (holdersAccStatus.state === HoldersUserState.NeedEnrollment) {
return true;
}
@@ -62,12 +62,12 @@ export const HoldersPrepaidCard = memo((props: {
props.onBeforeOpen?.();
if (needsEnrollment) {
- const onEnrollType: HoldersAppParams = { type: 'prepaid', id: card.id };
+ const onEnrollType: HoldersAppParams = { type: HoldersAppParamsType.Prepaid, id: card.id };
navigation.navigateHoldersLanding({ endpoint: url, onEnrollType }, props.isTestnet);
return;
}
- navigation.navigateHolders({ type: 'prepaid', id: card.id }, props.isTestnet);
+ navigation.navigateHolders({ type: HoldersAppParamsType.Prepaid, id: card.id }, props.isTestnet);
}, [card, needsEnrollment, props.onBeforeOpen, props.isTestnet]);
const { onPressIn, onPressOut, animatedStyle } = useAnimatedPressedInOut();
diff --git a/app/components/products/ProductsComponent.tsx b/app/components/products/ProductsComponent.tsx
index af64ef000..6032e4239 100644
--- a/app/components/products/ProductsComponent.tsx
+++ b/app/components/products/ProductsComponent.tsx
@@ -13,7 +13,7 @@ import { JettonsHiddenComponent } from "./JettonsHiddenComponent"
import { SelectedAccount } from "../../engine/types"
import { DappsRequests } from "../../fragments/wallet/products/DappsRequests"
import { ProductBanner } from "./ProductBanner"
-import { HoldersAccountState, holdersUrl } from "../../engine/api/holders/fetchAccountState"
+import { HoldersUserState, holdersUrl } from "../../engine/api/holders/fetchUserState"
import { PendingTransactions } from "../../fragments/wallet/views/PendingTransactions"
import { Typography } from "../styles"
import { useBanners } from "../../engine/hooks/banners"
@@ -22,7 +22,8 @@ import { MixpanelEvent, trackEvent } from "../../analytics/mixpanel"
import { AddressFormatUpdate } from "./AddressFormatUpdate"
import { TonProductComponent } from "./TonProductComponent"
import { SpecialJettonProduct } from "./SpecialJettonProduct"
-import { useIsHoldersWhitelisted } from "../../engine/hooks/holders/useIsHoldersWhitelisted"
+import { useIsHoldersInvited } from "../../engine/hooks/holders/useIsHoldersInvited"
+import { HoldersAppParamsType } from "../../fragments/holders/HoldersAppFragment"
import OldWalletIcon from '@assets/ic_old_wallet.svg';
@@ -37,11 +38,11 @@ export const ProductsComponent = memo(({ selected }: { selected: SelectedAccount
const banners = useBanners();
const url = holdersUrl(isTestnet);
const isHoldersReady = useIsConnectAppReady(url);
- const isHoldersWhitelisted = useIsHoldersWhitelisted(selected!.address, isTestnet);
- const showHoldersBuiltInBanner = (holdersAccounts?.accounts?.length ?? 0) === 0 && isHoldersWhitelisted;
+ const isHoldersInvited = useIsHoldersInvited(selected!.address, isTestnet);
+ const showHoldersBuiltInBanner = (holdersAccounts?.accounts?.length ?? 0) === 0 && isHoldersInvited;
const needsEnrolment = useMemo(() => {
- if (holdersAccStatus?.state === HoldersAccountState.NeedEnrollment) {
+ if (holdersAccStatus?.state === HoldersUserState.NeedEnrollment) {
return true;
}
return false;
@@ -67,10 +68,10 @@ export const ProductsComponent = memo(({ selected }: { selected: SelectedAccount
const onHoldersPress = useCallback(() => {
if (needsEnrolment || !isHoldersReady) {
- navigation.navigateHoldersLanding({ endpoint: url, onEnrollType: { type: 'create' } }, isTestnet);
+ navigation.navigateHoldersLanding({ endpoint: url, onEnrollType: { type: HoldersAppParamsType.Create } }, isTestnet);
return;
}
- navigation.navigateHolders({ type: 'create' }, isTestnet);
+ navigation.navigateHolders({ type: HoldersAppParamsType.Create }, isTestnet);
}, [needsEnrolment, isHoldersReady, isTestnet]);
const onProductBannerPress = useCallback((product: ProductAd) => {
@@ -102,7 +103,7 @@ export const ProductsComponent = memo(({ selected }: { selected: SelectedAccount
- {(!isHoldersWhitelisted && !!banners?.product) && (
+ {(!isHoldersInvited && !!banners?.product) && (
{t('common.balances')}
diff --git a/app/components/toast/ToastProvider.tsx b/app/components/toast/ToastProvider.tsx
index 9262d06ce..6dd0a3e0d 100644
--- a/app/components/toast/ToastProvider.tsx
+++ b/app/components/toast/ToastProvider.tsx
@@ -115,7 +115,7 @@ export const Toast = memo(({
{Icon && (
)}
-
+
{message}
diff --git a/app/components/webview/DAppWebView.tsx b/app/components/webview/DAppWebView.tsx
index 5853c8e16..dd0dd2fc0 100644
--- a/app/components/webview/DAppWebView.tsx
+++ b/app/components/webview/DAppWebView.tsx
@@ -1,5 +1,5 @@
import { ForwardedRef, RefObject, forwardRef, memo, useCallback, useEffect, useMemo, useReducer, useState } from "react";
-import { KeyboardAvoidingView, Platform, View, StyleSheet, ActivityIndicator, BackHandler } from "react-native";
+import { KeyboardAvoidingView, Platform, View, StyleSheet, ActivityIndicator, BackHandler, Linking } from "react-native";
import WebView, { WebViewMessageEvent, WebViewNavigation, WebViewProps } from "react-native-webview";
import { useNetwork, useTheme } from "../../engine/hooks";
import { WebViewErrorComponent } from "./WebViewErrorComponent";
@@ -22,10 +22,11 @@ import DeviceInfo from 'react-native-device-info';
import { processEmitterMessage } from "./utils/processEmitterMessage";
import { getLastAuthTimestamp, useKeysAuth } from "../secure/AuthWalletKeys";
import { getLockAppWithAuthState } from "../../engine/state/lockAppWithAuthState";
-import { useLockAppWithAuthState } from "../../engine/hooks/settings";
import WalletService, { addCardRequestSchema } from "../../modules/WalletService";
import { getHoldersToken } from "../../engine/hooks/holders/useHoldersAccountStatus";
import { getCurrentAddress } from "../../storage/appState";
+import { WebViewSourceUri } from "react-native-webview/lib/WebViewTypes";
+import { holdersUrl } from "../../engine/api/holders/fetchUserState";
export type DAppWebViewProps = WebViewProps & {
useMainButton?: boolean;
@@ -72,7 +73,6 @@ export const DAppWebView = memo(forwardRef((props: DAppWebViewProps, ref: Forwar
const navigation = useTypedNavigation();
const toaster = useToaster();
const markRefIdShown = useMarkBannerHidden();
- const [, setLockAppWithAuth] = useLockAppWithAuthState();
const [loaded, setLoaded] = useState(false);
@@ -98,15 +98,28 @@ export const DAppWebView = memo(forwardRef((props: DAppWebViewProps, ref: Forwar
}
);
+
+
const safelyOpenUrl = useCallback((url: string) => {
try {
+ const scheme = new URL(url).protocol.replace(':', '');
+ const sourceUrl = (props.source as WebViewSourceUri)?.uri;
+
+ if (
+ scheme === 'tg'
+ && !!sourceUrl
+ && sourceUrl.startsWith(holdersUrl(isTestnet))
+ ) {
+ Linking.openURL(url);
+ return;
+ }
let pageDomain = extractDomain(url);
if (isSafeDomain(pageDomain)) {
openWithInApp(url);
return;
}
} catch { }
- }, []);
+ }, [props.source]);
const onNavigation = useCallback((url: string) => {
if (!props.useQueryAPI) {
@@ -331,11 +344,7 @@ export const DAppWebView = memo(forwardRef((props: DAppWebViewProps, ref: Forwar
// Basic open url
if (data.name === 'openUrl' && data.args.url) {
try {
- let pageDomain = extractDomain(data.args.url);
- if (isSafeDomain(pageDomain)) {
- openWithInApp(data.args.url);
- return;
- }
+ safelyOpenUrl(data.args.url);
} catch {
warn('Failed to open url');
return;
diff --git a/app/components/webview/utils/processEmitterMessage.ts b/app/components/webview/utils/processEmitterMessage.ts
index 08fcc7c11..1b7c79291 100644
--- a/app/components/webview/utils/processEmitterMessage.ts
+++ b/app/components/webview/utils/processEmitterMessage.ts
@@ -20,7 +20,7 @@ export function processEmitterMessage(
case DAppEmitterEvents.APP_READY:
setTimeout(() => {
setLoaded(true);
- }, 200);
+ }, 100);
break;
default:
break;
diff --git a/app/engine/api/holders/fetchAccounts.ts b/app/engine/api/holders/fetchAccounts.ts
index bce349e07..fb0919e3b 100644
--- a/app/engine/api/holders/fetchAccounts.ts
+++ b/app/engine/api/holders/fetchAccounts.ts
@@ -1,7 +1,7 @@
import axios from "axios";
import { Address } from "@ton/core";
import { z } from "zod";
-import { holdersEndpoint } from "./fetchAccountState";
+import { holdersEndpoint } from "./fetchUserState";
const networksSchema = z.union([
z.literal('ton-mainnet'),
diff --git a/app/engine/api/holders/fetchAddressInviteCheck.ts b/app/engine/api/holders/fetchAddressInviteCheck.ts
new file mode 100644
index 000000000..a7d075a0a
--- /dev/null
+++ b/app/engine/api/holders/fetchAddressInviteCheck.ts
@@ -0,0 +1,60 @@
+import axios from "axios";
+import { holdersEndpoint } from "./fetchUserState";
+import { z } from "zod";
+import { Address } from "@ton/core";
+
+const inviteCheckCodec = z.object({
+ allowed: z.boolean(),
+});
+
+export async function fetchAddressInviteCheck(
+ address: string,
+ isTestnet: boolean
+) {
+ const endpoint = holdersEndpoint(isTestnet);
+ const formattedAddress = Address.parse(address).toString({
+ testOnly: isTestnet,
+ });
+
+ try {
+ let res = await axios.post(`https://${endpoint}/v2/invite/wallet/check`, {
+ wallet: formattedAddress,
+ network: isTestnet ? "ton-testnet" : "ton-mainnet",
+ });
+
+ if (res.status >= 400) {
+ res = await axios.post(`https://${endpoint}/v2/whitelist/wallet/check`, {
+ wallet: formattedAddress,
+ });
+ }
+
+ const parsed = inviteCheckCodec.safeParse(res.data);
+
+ if (!parsed.success) {
+ console.warn("Failed to parse invite check response", parsed.error);
+ return false;
+ }
+
+ return parsed.data.allowed;
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ if (error.response!.status >= 400) {
+ const res = await axios.post(
+ `https://${endpoint}/v2/whitelist/wallet/check`,
+ {
+ wallet: formattedAddress,
+ }
+ );
+
+ const parsed = inviteCheckCodec.safeParse(res.data);
+
+ if (!parsed.success) {
+ console.warn("Failed to parse invite check response", parsed.error);
+ return false;
+ }
+
+ return parsed.data.allowed;
+ }
+ }
+ }
+}
diff --git a/app/engine/api/holders/fetchAddressWhitelistCheck.ts b/app/engine/api/holders/fetchAddressWhitelistCheck.ts
deleted file mode 100644
index 3a3d94908..000000000
--- a/app/engine/api/holders/fetchAddressWhitelistCheck.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import axios from "axios";
-import { holdersEndpoint } from "./fetchAccountState";
-import { z } from "zod";
-
-const whitelistCheckCodec = z.object({
- allowed: z.boolean(),
-});
-
-export async function fetchAddressWhitelistCheck(address: string, isTestnet: boolean) {
- const endpoint = holdersEndpoint(isTestnet);
- const res = await axios.post(`https://${endpoint}/v2/whitelist/wallet/check`, { wallet: address });
-
- const parsed = whitelistCheckCodec.safeParse(res.data);
-
- if (!parsed.success) {
- console.warn('Failed to parse whitelist check response', parsed.error);
- return false;
- }
-
- return parsed.data.allowed;
-}
\ No newline at end of file
diff --git a/app/engine/api/holders/fetchApplePayCredentials.ts b/app/engine/api/holders/fetchApplePayCredentials.ts
index d869f9059..67febfa51 100644
--- a/app/engine/api/holders/fetchApplePayCredentials.ts
+++ b/app/engine/api/holders/fetchApplePayCredentials.ts
@@ -1,5 +1,5 @@
import axios from "axios";
-import { holdersEndpoint } from "./fetchAccountState";
+import { holdersEndpoint } from "./fetchUserState";
import { z } from "zod";
const cardCredentialsCodec = z.object({
diff --git a/app/engine/api/holders/fetchCardItem.ts b/app/engine/api/holders/fetchCardItem.ts
index 9321669ea..14598b9d4 100644
--- a/app/engine/api/holders/fetchCardItem.ts
+++ b/app/engine/api/holders/fetchCardItem.ts
@@ -1,6 +1,6 @@
import axios from "axios";
import * as t from "io-ts";
-import { holdersEndpoint } from "./fetchAccountState";
+import { holdersEndpoint } from "./fetchUserState";
export const cardItemCodec = t.type({
ok: t.boolean,
diff --git a/app/engine/api/holders/fetchCardsTransactions.ts b/app/engine/api/holders/fetchCardsTransactions.ts
index 468e28103..05b981ba3 100644
--- a/app/engine/api/holders/fetchCardsTransactions.ts
+++ b/app/engine/api/holders/fetchCardsTransactions.ts
@@ -1,5 +1,5 @@
import axios from "axios";
-import { holdersEndpoint } from "./fetchAccountState";
+import { holdersEndpoint } from "./fetchUserState";
export type CountryCode = 'AC' | 'AD' | 'AE' | 'AF' | 'AG' | 'AI' | 'AL' | 'AM' | 'AO' | 'AR' | 'AS' | 'AT' | 'AU' | 'AW' | 'AX' | 'AZ' | 'BA' | 'BB' | 'BD' | 'BE' | 'BF' | 'BG' | 'BH' | 'BI' | 'BJ' | 'BL' | 'BM' | 'BN' | 'BO' | 'BQ' | 'BR' | 'BS' | 'BT' | 'BW' | 'BY' | 'BZ' | 'CA' | 'CC' | 'CD' | 'CF' | 'CG' | 'CH' | 'CI' | 'CK' | 'CL' | 'CM' | 'CN' | 'CO' | 'CR' | 'CU' | 'CV' | 'CW' | 'CX' | 'CY' | 'CZ' | 'DE' | 'DJ' | 'DK' | 'DM' | 'DO' | 'DZ' | 'EC' | 'EE' | 'EG' | 'EH' | 'ER' | 'ES' | 'ET' | 'FI' | 'FJ' | 'FK' | 'FM' | 'FO' | 'FR' | 'GA' | 'GB' | 'GD' | 'GE' | 'GF' | 'GG' | 'GH' | 'GI' | 'GL' | 'GM' | 'GN' | 'GP' | 'GQ' | 'GR' | 'GT' | 'GU' | 'GW' | 'GY' | 'HK' | 'HN' | 'HR' | 'HT' | 'HU' | 'ID' | 'IE' | 'IL' | 'IM' | 'IN' | 'IO' | 'IQ' | 'IR' | 'IS' | 'IT' | 'JE' | 'JM' | 'JO' | 'JP' | 'KE' | 'KG' | 'KH' | 'KI' | 'KM' | 'KN' | 'KP' | 'KR' | 'KW' | 'KY' | 'KZ' | 'LA' | 'LB' | 'LC' | 'LI' | 'LK' | 'LR' | 'LS' | 'LT' | 'LU' | 'LV' | 'LY' | 'MA' | 'MC' | 'MD' | 'ME' | 'MF' | 'MG' | 'MH' | 'MK' | 'ML' | 'MM' | 'MN' | 'MO' | 'MP' | 'MQ' | 'MR' | 'MS' | 'MT' | 'MU' | 'MV' | 'MW' | 'MX' | 'MY' | 'MZ' | 'NA' | 'NC' | 'NE' | 'NF' | 'NG' | 'NI' | 'NL' | 'NO' | 'NP' | 'NR' | 'NU' | 'NZ' | 'OM' | 'PA' | 'PE' | 'PF' | 'PG' | 'PH' | 'PK' | 'PL' | 'PM' | 'PR' | 'PS' | 'PT' | 'PW' | 'PY' | 'QA' | 'RE' | 'RO' | 'RS' | 'RU' | 'RW' | 'SA' | 'SB' | 'SC' | 'SD' | 'SE' | 'SG' | 'SH' | 'SI' | 'SJ' | 'SK' | 'SL' | 'SM' | 'SN' | 'SO' | 'SR' | 'SS' | 'ST' | 'SV' | 'SX' | 'SY' | 'SZ' | 'TA' | 'TC' | 'TD' | 'TG' | 'TH' | 'TJ' | 'TK' | 'TL' | 'TM' | 'TN' | 'TO' | 'TR' | 'TT' | 'TV' | 'TW' | 'TZ' | 'UA' | 'UG' | 'US' | 'UY' | 'UZ' | 'VA' | 'VC' | 'VE' | 'VG' | 'VI' | 'VN' | 'VU' | 'WF' | 'WS' | 'XK' | 'YE' | 'YT' | 'ZA' | 'ZM' | 'ZW';
diff --git a/app/engine/api/holders/fetchAccountState.ts b/app/engine/api/holders/fetchUserState.ts
similarity index 71%
rename from app/engine/api/holders/fetchAccountState.ts
rename to app/engine/api/holders/fetchUserState.ts
index 27bdb6e80..6c914e49b 100644
--- a/app/engine/api/holders/fetchAccountState.ts
+++ b/app/engine/api/holders/fetchUserState.ts
@@ -10,11 +10,11 @@ export const holdersUrlProd = 'https://tonhub.holders.io';
export const holdersEndpoint = (isTestnet: boolean) => isTestnet ? holdersEndpointStage : holdersEndpointProd;
export const holdersUrl = (isTestnet: boolean) => isTestnet ? holdersUrlStage : holdersUrlProd;
-export type AccountState = z.infer;
+export type UserState = z.infer;
-export type AccountStateRes = { ok: boolean, state: AccountState };
+export type UserStateRes = { ok: boolean, state: UserState };
-export enum HoldersAccountState {
+export enum HoldersUserState {
NeedEnrollment = 'need-enrollment',
NeedPhone = 'need-phone',
NoRef = 'no-ref',
@@ -23,15 +23,15 @@ export enum HoldersAccountState {
Ok = 'ok',
}
-export const accountStateCodec = z.union([
+export const userStateCodec = z.union([
z.object({
state: z.union([
- z.literal(HoldersAccountState.NeedPhone),
- z.literal(HoldersAccountState.NoRef),
+ z.literal(HoldersUserState.NeedPhone),
+ z.literal(HoldersUserState.NoRef),
]),
}),
z.object({
- state: z.literal(HoldersAccountState.NeedKyc),
+ state: z.literal(HoldersUserState.NeedKyc),
kycStatus: z.union([
z.null(),
z.object({
@@ -53,9 +53,9 @@ export const accountStateCodec = z.union([
}),
z.object({
state: z.union([
- z.literal(HoldersAccountState.Ok),
- z.literal(HoldersAccountState.NeedEmail),
- z.literal(HoldersAccountState.NeedPhone),
+ z.literal(HoldersUserState.Ok),
+ z.literal(HoldersUserState.NeedEmail),
+ z.literal(HoldersUserState.NeedPhone),
]),
notificationSettings: z.object({
enabled: z.boolean(),
@@ -64,12 +64,12 @@ export const accountStateCodec = z.union([
}),
]);
-export const accountStateResCodec = z.object({
+export const userStateResCodec = z.object({
ok: z.boolean(),
- state: accountStateCodec,
+ state: userStateCodec,
});
-export async function fetchAccountState(token: string, isTestnet: boolean): Promise {
+export async function fetchUserState(token: string, isTestnet: boolean): Promise {
const endpoint = isTestnet ? holdersEndpointStage : holdersEndpointProd;
const res = await axios.post(`https://${endpoint}/account/state`, { token });
@@ -81,5 +81,5 @@ export async function fetchAccountState(token: string, isTestnet: boolean): Prom
throw Error('Failed to fetch account state');
}
- return res.data.state as AccountState;
+ return res.data.state as UserState;
}
\ No newline at end of file
diff --git a/app/engine/api/holders/fetchAccountToken.ts b/app/engine/api/holders/fetchUserToken.ts
similarity index 87%
rename from app/engine/api/holders/fetchAccountToken.ts
rename to app/engine/api/holders/fetchUserToken.ts
index a1cf12afc..270a45bbb 100644
--- a/app/engine/api/holders/fetchAccountToken.ts
+++ b/app/engine/api/holders/fetchUserToken.ts
@@ -1,5 +1,5 @@
import axios from 'axios';
-import { holdersEndpoint } from './fetchAccountState';
+import { holdersEndpoint } from './fetchUserState';
import { z } from 'zod';
const tonconnectV2Config = z.object({
@@ -57,12 +57,13 @@ const keys = z.union([tonXKey, tonXLiteKey, tonconnectV2Key]);
export type AccountKeyParam = z.infer;
-export async function fetchAccountToken(key: AccountKeyParam, isTestnet: boolean): Promise {
+export async function fetchUserToken(key: AccountKeyParam, isTestnet: boolean, inviteId?: string): Promise {
const endpoint = holdersEndpoint(isTestnet);
const requestParams = {
stack: 'ton',
network: isTestnet ? 'ton-testnet' : 'ton-mainnet',
- key: key
+ key: key,
+ inviteId
};
const res = await axios.post(
@@ -71,7 +72,7 @@ export async function fetchAccountToken(key: AccountKeyParam, isTestnet: boolean
);
if (!res.data.ok) {
- throw Error('Failed to fetch card token');
+ throw Error('Failed to fetch user token');
}
return res.data.token as string;
}
\ No newline at end of file
diff --git a/app/engine/clients.ts b/app/engine/clients.ts
index edf61f8b5..ffc950e82 100644
--- a/app/engine/clients.ts
+++ b/app/engine/clients.ts
@@ -1,10 +1,23 @@
import { TonClient4 } from '@ton/ton';
import { QueryClient } from '@tanstack/react-query';
+import { Platform } from 'react-native';
+import * as Application from 'expo-application';
+
+
+const requestInterceptorMainnet = (config: any) => {
+ config.headers['User-Agent'] = `Tonhub/${Application.nativeApplicationVersion} ${Platform.OS}`;
+ return config;
+};
+
+const requestInterceptorTestnet = (config: any) => {
+ config.headers['User-Agent'] = `Tonhub/${Application.nativeApplicationVersion} ${Platform.OS} (testnet)`;
+ return config;
+};
export const clients = {
ton: {
- testnet: new TonClient4({ endpoint: 'https://testnet-v4.tonhubapi.com', timeout: 5000 }),
- mainnet: new TonClient4({ endpoint: 'https://mainnet-v4.tonhubapi.com', timeout: 5000 }),
+ testnet: new TonClient4({ endpoint: 'https://testnet-v4.tonhubapi.com', timeout: 5000, requestInterceptor: requestInterceptorTestnet }),
+ mainnet: new TonClient4({ endpoint: 'https://mainnet-v4.tonhubapi.com', timeout: 5000, requestInterceptor: requestInterceptorMainnet }),
}
}
diff --git a/app/engine/effects/onHoldersEnroll.ts b/app/engine/effects/onHoldersEnroll.ts
index 4ae8045ac..3113ae4c9 100644
--- a/app/engine/effects/onHoldersEnroll.ts
+++ b/app/engine/effects/onHoldersEnroll.ts
@@ -2,7 +2,7 @@ import { Address } from "@ton/ton";
import { queryClient } from "../clients";
import { Queries } from "../queries";
import { getHoldersToken } from "../hooks/holders/useHoldersAccountStatus";
-import { HoldersAccountState, fetchAccountState } from "../api/holders/fetchAccountState";
+import { HoldersUserState, fetchUserState } from "../api/holders/fetchUserState";
import { fetchAccountsPublic, fetchAccountsList } from "../api/holders/fetchAccounts";
import { updateProvisioningCredentials } from "../holders/updateProvisioningCredentials";
@@ -12,17 +12,17 @@ export async function onHoldersEnroll(account: string, isTestnet: boolean) {
const token = getHoldersToken(address);
if (!token) {
- return { state: HoldersAccountState.NeedEnrollment } as { state: HoldersAccountState.NeedEnrollment }; // This looks amazingly stupid
+ return { state: HoldersUserState.NeedEnrollment } as { state: HoldersUserState.NeedEnrollment }; // This looks amazingly stupid
}
- const fetched = await fetchAccountState(token, isTestnet);
+ const fetched = await fetchUserState(token, isTestnet);
if (!fetched) {
- return { state: HoldersAccountState.NeedEnrollment } as { state: HoldersAccountState.NeedEnrollment };
+ return { state: HoldersUserState.NeedEnrollment } as { state: HoldersUserState.NeedEnrollment };
}
const status = { ...fetched, token };
queryClient.setQueryData(Queries.Holders(address).Status(), () => status);
- if (status.state === HoldersAccountState.Ok) {
+ if (status.state === HoldersUserState.Ok) {
let accounts;
let prepaidCards;
let type = 'public';
diff --git a/app/engine/holders/watchHoldersAccountUpdates.ts b/app/engine/holders/watchHoldersAccountUpdates.ts
index 1383bf4e8..fba7be9e0 100644
--- a/app/engine/holders/watchHoldersAccountUpdates.ts
+++ b/app/engine/holders/watchHoldersAccountUpdates.ts
@@ -1,5 +1,5 @@
import { createLogger } from '../../utils/log';
-import { holdersEndpoint } from '../api/holders/fetchAccountState';
+import { holdersEndpoint } from '../api/holders/fetchUserState';
let index = 0;
diff --git a/app/engine/holdersWatcher.ts b/app/engine/holdersWatcher.ts
index 8ebe6d45b..b815646d4 100644
--- a/app/engine/holdersWatcher.ts
+++ b/app/engine/holdersWatcher.ts
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useHoldersAccountStatus } from "./hooks/holders/useHoldersAccountStatus";
import { useSelectedAccount } from "./hooks/appstate/useSelectedAccount";
-import { HoldersAccountState } from "./api/holders/fetchAccountState";
+import { HoldersUserState } from "./api/holders/fetchUserState";
import { useNetwork } from "./hooks/network/useNetwork";
import { watchHoldersAccountUpdates } from './holders/watchHoldersAccountUpdates';
import { queryClient } from "./clients";
@@ -14,8 +14,8 @@ export function useHoldersWatcher() {
const status = useHoldersAccountStatus(account?.address.toString({ testOnly: isTestnet }) ?? '');
const cards = useHoldersAccounts(account?.address.toString({ testOnly: isTestnet }) ?? '');
- useEffect(() => {
- if (status?.data?.state !== HoldersAccountState.Ok) {
+ useEffect(() => {
+ if (status?.data?.state !== HoldersUserState.Ok) {
return;
}
diff --git a/app/engine/hooks/holders/useCardTransactions.ts b/app/engine/hooks/holders/useCardTransactions.ts
index e6cd9c6a1..e7e6eee2c 100644
--- a/app/engine/hooks/holders/useCardTransactions.ts
+++ b/app/engine/hooks/holders/useCardTransactions.ts
@@ -2,7 +2,7 @@ import { useInfiniteQuery } from "@tanstack/react-query";
import { CardNotification, fetchCardsTransactions } from "../../api/holders/fetchCardsTransactions";
import { Queries } from "../../queries";
import { useHoldersAccountStatus } from "./useHoldersAccountStatus";
-import { HoldersAccountState } from "../../api/holders/fetchAccountState";
+import { HoldersUserState } from "../../api/holders/fetchUserState";
export function useCardTransactions(address: string, id: string) {
let status = useHoldersAccountStatus(address).data;
@@ -23,7 +23,7 @@ export function useCardTransactions(address: string, id: string) {
};
},
queryFn: async (ctx) => {
- if (!!status && status.state !== HoldersAccountState.NeedEnrollment) {
+ if (!!status && status.state !== HoldersUserState.NeedEnrollment) {
const cardRes = await fetchCardsTransactions(status.token, id, 40, ctx.pageParam?.lastCursor, 'desc');
if (!!cardRes) {
return cardRes;
diff --git a/app/engine/hooks/holders/useClearHolders.ts b/app/engine/hooks/holders/useClearHolders.ts
index a5a674c7f..15fe88925 100644
--- a/app/engine/hooks/holders/useClearHolders.ts
+++ b/app/engine/hooks/holders/useClearHolders.ts
@@ -1,4 +1,4 @@
-import { holdersUrl } from "../../api/holders/fetchAccountState";
+import { holdersUrl } from "../../api/holders/fetchUserState";
import { useDomainKeys } from "../dapps/useDomainKeys";
import { deleteHoldersToken } from "./useHoldersAccountStatus";
import { extractDomain } from "../../utils/extractDomain";
diff --git a/app/engine/hooks/holders/useHasHoldersProducts.ts b/app/engine/hooks/holders/useHasHoldersProducts.ts
index e4e312900..d4c48777f 100644
--- a/app/engine/hooks/holders/useHasHoldersProducts.ts
+++ b/app/engine/hooks/holders/useHasHoldersProducts.ts
@@ -4,7 +4,7 @@ import { Queries } from "../../queries";
import { queryClient } from "../../clients";
import { getQueryData } from "../../utils/getQueryData";
import { HoldersAccountStatus } from "./useHoldersAccountStatus";
-import { HoldersAccountState } from "../../api/holders/fetchAccountState";
+import { HoldersUserState } from "../../api/holders/fetchUserState";
import { HoldersAccounts } from "./useHoldersAccounts";
function hasAccounts(accs: HoldersAccounts | undefined) {
@@ -24,8 +24,8 @@ export function getHasHoldersProducts(address: string) {
const token = (
!!status &&
- status.state !== HoldersAccountState.NoRef &&
- status.state !== HoldersAccountState.NeedEnrollment
+ status.state !== HoldersUserState.NoRef &&
+ status.state !== HoldersUserState.NeedEnrollment
) ? status.token : null;
const accounts = getQueryData(queryCache, Queries.Holders(address).Cards(!!token ? 'private' : 'public'));
diff --git a/app/engine/hooks/holders/useHoldersAccountStatus.ts b/app/engine/hooks/holders/useHoldersAccountStatus.ts
index cfe841fbb..aeeb04ff6 100644
--- a/app/engine/hooks/holders/useHoldersAccountStatus.ts
+++ b/app/engine/hooks/holders/useHoldersAccountStatus.ts
@@ -4,13 +4,13 @@ import { Address } from "@ton/core";
import { useMemo } from "react";
import { useNetwork } from "../network/useNetwork";
import { storage } from "../../../storage/storage";
-import { HoldersAccountState, accountStateCodec, fetchAccountState } from "../../api/holders/fetchAccountState";
+import { HoldersUserState, userStateCodec, fetchUserState } from "../../api/holders/fetchUserState";
import { z } from 'zod';
import { removeProvisioningCredentials } from "../../holders/updateProvisioningCredentials";
const holdersAccountStatus = z.union([
- z.object({ state: z.literal(HoldersAccountState.NeedEnrollment) }),
- z.intersection(z.object({ token: z.string() }), accountStateCodec),
+ z.object({ state: z.literal(HoldersUserState.NeedEnrollment) }),
+ z.intersection(z.object({ token: z.string() }), userStateCodec),
]);
export type HoldersAccountStatus = z.infer;
@@ -63,13 +63,13 @@ export function useHoldersAccountStatus(address: string | Address) {
const token = getHoldersToken(addr);
if (!token) {
- return { state: HoldersAccountState.NeedEnrollment } as HoldersAccountStatus; // This looks amazingly stupid
+ return { state: HoldersUserState.NeedEnrollment } as HoldersAccountStatus; // This looks amazingly stupid
}
- const fetched = await fetchAccountState(token, isTestnet);
+ const fetched = await fetchUserState(token, isTestnet);
if (!fetched) {
- return { state: HoldersAccountState.NeedEnrollment } as HoldersAccountStatus;
+ return { state: HoldersUserState.NeedEnrollment } as HoldersAccountStatus;
}
return { ...fetched, token } as HoldersAccountStatus;
diff --git a/app/engine/hooks/holders/useHoldersAccounts.ts b/app/engine/hooks/holders/useHoldersAccounts.ts
index f8507d2ef..6a15f01bb 100644
--- a/app/engine/hooks/holders/useHoldersAccounts.ts
+++ b/app/engine/hooks/holders/useHoldersAccounts.ts
@@ -5,7 +5,7 @@ import { useQuery } from "@tanstack/react-query";
import { Queries } from "../../queries";
import { GeneralHoldersAccount, PrePaidHoldersCard, fetchAccountsList, fetchAccountsPublic } from "../../api/holders/fetchAccounts";
import { useHoldersAccountStatus } from "./useHoldersAccountStatus";
-import { HoldersAccountState } from "../../api/holders/fetchAccountState";
+import { HoldersUserState } from "../../api/holders/fetchUserState";
import { updateProvisioningCredentials } from "../../holders/updateProvisioningCredentials";
export type HoldersAccounts = {
@@ -27,8 +27,8 @@ export function useHoldersAccounts(address: string | Address) {
const token = (
!!status &&
- status.state !== HoldersAccountState.NoRef &&
- status.state !== HoldersAccountState.NeedEnrollment
+ status.state !== HoldersUserState.NoRef &&
+ status.state !== HoldersUserState.NeedEnrollment
) ? status.token : null;
let query = useQuery({
diff --git a/app/engine/hooks/holders/useHoldersEnroll.ts b/app/engine/hooks/holders/useHoldersEnroll.ts
index 955eeec62..59e5db53d 100644
--- a/app/engine/hooks/holders/useHoldersEnroll.ts
+++ b/app/engine/hooks/holders/useHoldersEnroll.ts
@@ -1,11 +1,11 @@
import { Address, beginCell, storeStateInit } from "@ton/core";
import { AuthParams, AuthWalletKeysType } from "../../../components/secure/AuthWalletKeys";
-import { fetchAccountToken } from "../../api/holders/fetchAccountToken";
+import { fetchUserToken } from "../../api/holders/fetchUserToken";
import { contractFromPublicKey } from "../../contractFromPublicKey";
import { onHoldersEnroll } from "../../effects/onHoldersEnroll";
import { WalletKeys } from "../../../storage/walletKeys";
import { ConnectReplyBuilder } from "../../tonconnect/ConnectReplyBuilder";
-import { holdersUrl } from "../../api/holders/fetchAccountState";
+import { holdersUrl } from "../../api/holders/fetchUserState";
import { getAppManifest } from "../../getters/getAppManifest";
import { AppManifest } from "../../api/fetchManifest";
import { ConnectItemReply, TonProofItemReplySuccess } from "@tonconnect/protocol";
@@ -24,7 +24,8 @@ export type HoldersEnrollParams = {
},
domain: string,
authContext: AuthWalletKeysType,
- authStyle?: AuthParams | undefined
+ authStyle?: AuthParams | undefined,
+ inviteId?: string
}
export enum HoldersEnrollErrorType {
@@ -41,7 +42,7 @@ export enum HoldersEnrollErrorType {
export type HoldersEnrollResult = { type: 'error', error: HoldersEnrollErrorType } | { type: 'success' };
-export function useHoldersEnroll({ acc, authContext, authStyle }: HoldersEnrollParams) {
+export function useHoldersEnroll({ acc, authContext, authStyle, inviteId }: HoldersEnrollParams) {
const { isTestnet } = useNetwork();
const saveAppConnection = useSaveAppConnection();
const connectApp = useConnectApp();
@@ -59,6 +60,15 @@ export function useHoldersEnroll({ acc, authContext, authStyle }: HoldersEnrollP
const connections = app ? connectAppConnections(extensionKey(app.url)) : [];
const isInjected = connections.find((item) => item.type === TonConnectBridgeType.Injected);
+ if (inviteId) {
+
+ //
+ // Reset holders token with every invite attempt
+ //
+
+ deleteHoldersToken(acc.address.toString({ testOnly: isTestnet }))
+ }
+
//
// Check holders token value
//
@@ -149,7 +159,7 @@ export function useHoldersEnroll({ acc, authContext, authStyle }: HoldersEnrollP
return { type: 'error', error: HoldersEnrollErrorType.NoProof };
}
- let token = await fetchAccountToken({
+ let token = await fetchUserToken({
kind: 'tonconnect-v2',
wallet: 'tonhub',
config: {
@@ -163,7 +173,7 @@ export function useHoldersEnroll({ acc, authContext, authStyle }: HoldersEnrollP
walletStateInit: stateInitStr
}
}
- }, isTestnet);
+ }, isTestnet, inviteId);
setHoldersToken(acc.address.toString({ testOnly: isTestnet }), token);
} catch {
diff --git a/app/engine/hooks/holders/useIsHoldersWhitelisted.ts b/app/engine/hooks/holders/useIsHoldersInvited.ts
similarity index 63%
rename from app/engine/hooks/holders/useIsHoldersWhitelisted.ts
rename to app/engine/hooks/holders/useIsHoldersInvited.ts
index 313c76e86..c4a10805b 100644
--- a/app/engine/hooks/holders/useIsHoldersWhitelisted.ts
+++ b/app/engine/hooks/holders/useIsHoldersInvited.ts
@@ -2,9 +2,9 @@ import { useQuery } from "@tanstack/react-query";
import { Address } from "@ton/core";
import { Queries } from "../../queries";
import { useMemo } from "react";
-import { fetchAddressWhitelistCheck } from "../../api/holders/fetchAddressWhitelistCheck";
+import { fetchAddressInviteCheck } from "../../api/holders/fetchAddressInviteCheck";
-export function useIsHoldersWhitelisted(address: string | Address, isTestnet: boolean) {
+export function useIsHoldersInvited(address: string | Address, isTestnet: boolean) {
const addressString = useMemo(() => {
if (address instanceof Address) {
return address.toString({ testOnly: isTestnet });
@@ -13,11 +13,11 @@ export function useIsHoldersWhitelisted(address: string | Address, isTestnet: bo
}, [address, isTestnet]);
const query = useQuery({
- queryKey: Queries.Holders(addressString).WhiteList(),
+ queryKey: Queries.Holders(addressString).Invite(),
refetchOnMount: true,
staleTime: 1000 * 60 * 5, // 5 minutes
queryFn: async (key) => {
- return await fetchAddressWhitelistCheck(addressString, isTestnet);
+ return await fetchAddressInviteCheck(addressString, isTestnet);
}
});
diff --git a/app/engine/queries.ts b/app/engine/queries.ts
index 2deeab8b0..72ce084f1 100644
--- a/app/engine/queries.ts
+++ b/app/engine/queries.ts
@@ -24,7 +24,7 @@ export const Queries = {
Status: () => ['holders', address, 'status'],
Cards: (mode: 'private' | 'public') => ['holders', address, 'cards', mode],
Notifications: (id: string) => ['holders', address, 'events', id],
- WhiteList: () => ['holders', address, 'whitelist'],
+ Invite: () => ['holders', address, 'invite'],
}),
ContractMetadata: (address: string) => (['contractMetadata', address]),
diff --git a/app/fragments/SettingsFragment.tsx b/app/fragments/SettingsFragment.tsx
index 00823afd4..08266de57 100644
--- a/app/fragments/SettingsFragment.tsx
+++ b/app/fragments/SettingsFragment.tsx
@@ -20,7 +20,7 @@ import { useFocusEffect, useRoute } from '@react-navigation/native';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useLedgerTransport } from './ledger/components/TransportContext';
import { Typography } from '../components/styles';
-import { HoldersAccountState, holdersUrl as resolveHoldersUrl } from '../engine/api/holders/fetchAccountState';
+import { HoldersUserState, holdersUrl as resolveHoldersUrl } from '../engine/api/holders/fetchUserState';
import IcSecurity from '@assets/settings/ic-security.svg';
import IcSpam from '@assets/settings/ic-spam.svg';
@@ -122,7 +122,7 @@ export const SettingsFragment = fragment(() => {
const queryCache = queryClient.getQueryCache();
const address = seleted!.address.toString({ testOnly: network.isTestnet });
const status = getQueryData(queryCache, Queries.Holders(address).Status());
- const token = status?.state === HoldersAccountState.Ok ? status.token : getHoldersToken(address);
+ const token = status?.state === HoldersUserState.Ok ? status.token : getHoldersToken(address);
const accountsStatus = getQueryData(queryCache, Queries.Holders(address).Cards(!!token ? 'private' : 'public'));
const initialState = {
diff --git a/app/fragments/apps/components/inject/createInjectSource.ts b/app/fragments/apps/components/inject/createInjectSource.ts
index 420c0ce49..d62a6314c 100644
--- a/app/fragments/apps/components/inject/createInjectSource.ts
+++ b/app/fragments/apps/components/inject/createInjectSource.ts
@@ -221,7 +221,7 @@ export const authAPI = (params: { lastAuthTime?: number, isSecured: boolean }) =
export const dappWalletAPI = `
window['dapp-wallet'] = (() => {
- let __DAPP_WALLET_AVAILIBLE = true;
+ let __DAPP_WALLET_AVAILABLE = true;
let inProgress = false;
let currentCallback = null;
@@ -290,7 +290,7 @@ window['dapp-wallet'] = (() => {
}
}
- const obj = { __DAPP_WALLET_AVAILIBLE, isEnabled, checkIfCardIsAlreadyAdded, canAddCard, addCardToWallet, __response };
+ const obj = { __DAPP_WALLET_AVAILABLE, isEnabled, checkIfCardIsAlreadyAdded, canAddCard, addCardToWallet, __response };
Object.freeze(obj);
return obj;
})();
diff --git a/app/fragments/holders/HoldersAppFragment.tsx b/app/fragments/holders/HoldersAppFragment.tsx
index e94f2849d..f2bfd597e 100644
--- a/app/fragments/holders/HoldersAppFragment.tsx
+++ b/app/fragments/holders/HoldersAppFragment.tsx
@@ -6,16 +6,27 @@ import { useParams } from '../../utils/useParams';
import { t } from '../../i18n/t';
import { useEffect, useMemo } from 'react';
import { useHoldersAccountStatus, useHoldersAccounts, useNetwork, useSelectedAccount, useTheme } from '../../engine/hooks';
-import { holdersUrl } from '../../engine/api/holders/fetchAccountState';
+import { holdersUrl } from '../../engine/api/holders/fetchUserState';
import { StatusBar, setStatusBarStyle } from 'expo-status-bar';
import { onHoldersInvalidate } from '../../engine/effects/onHoldersInvalidate';
import { useFocusEffect } from '@react-navigation/native';
+export enum HoldersAppParamsType {
+ Account = 'account',
+ Prepaid = 'prepaid',
+ Create = 'create',
+ Invite = 'invite',
+ Transactions = 'transactions',
+ Path = 'path'
+}
+
export type HoldersAppParams =
- | { type: 'account', id: string }
- | { type: 'prepaid', id: string }
- | { type: 'create' }
- | { type: 'transactions', query: { [key: string]: string | undefined } };
+ | { type: HoldersAppParamsType.Account, id: string }
+ | { type: HoldersAppParamsType.Prepaid, id: string }
+ | { type: HoldersAppParamsType.Create }
+ | { type: HoldersAppParamsType.Invite }
+ | { type: HoldersAppParamsType.Path, path: string, query: { [key: string]: string | undefined } }
+ | { type: HoldersAppParamsType.Transactions, query: { [key: string]: string | undefined } };
export const HoldersAppFragment = fragment(() => {
const theme = useTheme();
diff --git a/app/fragments/holders/HoldersLandingFragment.tsx b/app/fragments/holders/HoldersLandingFragment.tsx
index 9598deb1b..9d65a9024 100644
--- a/app/fragments/holders/HoldersLandingFragment.tsx
+++ b/app/fragments/holders/HoldersLandingFragment.tsx
@@ -6,11 +6,11 @@ import { useTypedNavigation } from '../../utils/useTypedNavigation';
import { t } from '../../i18n/t';
import { extractDomain } from '../../engine/utils/extractDomain';
import { useParams } from '../../utils/useParams';
-import { HoldersAppParams } from './HoldersAppFragment';
+import { HoldersAppParams, HoldersAppParamsType } from './HoldersAppFragment';
import { getLocales } from 'react-native-localize';
import { fragment } from '../../fragment';
import { useKeysAuth } from '../../components/secure/AuthWalletKeys';
-import { useCallback, useMemo, useRef } from 'react';
+import { useCallback, useMemo, useRef, useState } from 'react';
import { useNetwork, usePrimaryCurrency, useSelectedAccount } from '../../engine/hooks';
import { useTheme } from '../../engine/hooks';
import { useHoldersEnroll } from '../../engine/hooks';
@@ -22,6 +22,7 @@ import { HoldersEnrollErrorType } from '../../engine/hooks/holders/useHoldersEnr
import { DAppWebView, DAppWebViewProps } from '../../components/webview/DAppWebView';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { getAppManifest } from '../../engine/getters/getAppManifest';
+import { useActionSheet } from '@expo/react-native-action-sheet';
export const HoldersLandingFragment = fragment(() => {
const acc = useSelectedAccount()!;
@@ -29,14 +30,15 @@ export const HoldersLandingFragment = fragment(() => {
const { isTestnet } = useNetwork();
const webRef = useRef(null);
const authContext = useKeysAuth();
+ const { showActionSheetWithOptions } = useActionSheet();
const navigation = useTypedNavigation();
const safeArea = useSafeAreaInsets();
const [currency,] = usePrimaryCurrency();
- const { endpoint, onEnrollType } = useParams<{ endpoint: string, onEnrollType: HoldersAppParams }>();
+ const { endpoint, onEnrollType, inviteId } = useParams<{ endpoint: string, onEnrollType: HoldersAppParams, inviteId?: string }>();
const domain = extractDomain(endpoint);
- const enroll = useHoldersEnroll({ acc, domain, authContext, authStyle: { paddingTop: 32 } });
+ const enroll = useHoldersEnroll({ acc, domain, authContext, inviteId, authStyle: { paddingTop: 32 } });
const lang = getLocales()[0].languageCode;
// Anim
@@ -184,10 +186,45 @@ export const HoldersLandingFragment = fragment(() => {
}
}, [onContentProcessDidTerminate, onEnroll]);
+ const [renderKey, setRenderKey] = useState(0);
+
+ const onReload = useCallback(() => {
+ setRenderKey(renderKey + 1);
+ }, []);
+
+ const onSupport = useCallback(() => {
+ const tonhubOptions = [t('common.cancel'), t('settings.support.telegram'), t('settings.support.form')];
+ const cancelButtonIndex = 0;
+
+ const tonhubSupportSheet = () => {
+ showActionSheetWithOptions({
+ options: tonhubOptions,
+ title: t('settings.support.title'),
+ cancelButtonIndex,
+ }, (selectedIndex?: number) => {
+ switch (selectedIndex) {
+ case 1:
+ openWithInApp('https://t.me/WhalesSupportBot');
+ break;
+ case 2:
+ openWithInApp('https://airtable.com/appWErwfR8x0o7vmz/shr81d2H644BNUtPN');
+ break;
+ default:
+ break;
+ }
+ });
+ }
+
+ tonhubSupportSheet();
+ }, []);
+
return (
-
+
{
lockScroll: true
}}
webviewDebuggingEnabled={isTestnet}
- loader={(p) => }
+ loader={(p) => }
/>
void,
+ onSupport?: () => void,
+ showClose?: boolean
+}) => {
+ const safeArea = useSafeAreaInsets();
+ const animation = useSharedValue(0);
+
+ useEffect(() => {
+ animation.value = withRepeat(
+ withTiming(1, {
+ duration: 500,
+ easing: Easing.bezier(0.25, 0.1, 0.25, 1)
+ }),
+ 10,
+ true,
+ );
+ }, []);
+
+ const animatedStyles = useAnimatedStyle(() => {
+ const opacity = interpolate(
+ animation.value,
+ [0, 1],
+ [1, theme.style === 'dark' ? 0.75 : 1],
+ Extrapolation.CLAMP
+ );
+ const scale = interpolate(
+ animation.value,
+ [0, 1],
+ [1, 1.01],
+ Extrapolation.CLAMP
+ )
+ return {
+ opacity: opacity,
+ transform: [{ scale: scale }],
+ };
+ }, [theme.style]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(onReload || onSupport) && (
+
+
+
+ {t('products.holders.loadingLongerTitle')}
+
+
+ {t('products.holders.loadingLonger')}
+
+
+ {onReload && (
+
+
+
+ )}
+ {onSupport && (
+
+
+
+ )}
+
+
+ )}
+
+ );
+});
diff --git a/app/fragments/holders/components/HoldersAppComponent.tsx b/app/fragments/holders/components/HoldersAppComponent.tsx
index 645033810..7ef061e4c 100644
--- a/app/fragments/holders/components/HoldersAppComponent.tsx
+++ b/app/fragments/holders/components/HoldersAppComponent.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { Linking, Platform, View } from 'react-native';
+import { Linking, Platform, Pressable, View } from 'react-native';
import { ShouldStartLoadRequest } from 'react-native-webview/lib/WebViewTypes';
import { extractDomain } from '../../../engine/utils/extractDomain';
import { useTypedNavigation } from '../../../utils/useTypedNavigation';
@@ -9,15 +9,15 @@ import { protectNavigation } from '../../apps/components/protect/protectNavigati
import { getLocales } from 'react-native-localize';
import { useLinkNavigator } from '../../../useLinkNavigator';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { memo, useCallback, useEffect, useMemo, useState } from 'react';
-import { HoldersAppParams } from '../HoldersAppFragment';
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { HoldersAppParams, HoldersAppParamsType } from '../HoldersAppFragment';
import Animated, { Easing, Extrapolation, interpolate, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
import { useDAppBridge, usePrimaryCurrency } from '../../../engine/hooks';
import { useTheme } from '../../../engine/hooks';
import { useNetwork } from '../../../engine/hooks';
import { useSelectedAccount } from '../../../engine/hooks';
import { getCurrentAddress } from '../../../storage/appState';
-import { HoldersAccountState, holdersUrl } from '../../../engine/api/holders/fetchAccountState';
+import { HoldersUserState, holdersUrl } from '../../../engine/api/holders/fetchUserState';
import { HoldersAccountStatus, getHoldersToken } from '../../../engine/hooks/holders/useHoldersAccountStatus';
import { ScreenHeader } from '../../../components/ScreenHeader';
import { onHoldersInvalidate } from '../../../engine/effects/onHoldersInvalidate';
@@ -25,6 +25,11 @@ import { DAppWebView, DAppWebViewProps } from '../../../components/webview/DAppW
import { ThemeType } from '../../../engine/state/theme';
import { useDimensions } from '@react-native-community/hooks';
import { HoldersAccounts } from '../../../engine/hooks/holders/useHoldersAccounts';
+import { openWithInApp } from '../../../utils/openWithInApp';
+import { t } from '../../../i18n/t';
+import { useActionSheet } from '@expo/react-native-action-sheet';
+import { AccountPlaceholder } from './AccountPlaceholder';
+import { Image } from "expo-image";
export function normalizePath(path: string) {
return path.replaceAll('.', '_');
@@ -32,87 +37,6 @@ export function normalizePath(path: string) {
import IcHolders from '@assets/ic_holders.svg';
-const AccountPlaceholder = memo(({ theme }: { theme: ThemeType }) => {
- const safeArea = useSafeAreaInsets();
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-});
-
const CardPlaceholder = memo(({ theme }: { theme: ThemeType }) => {
const dimensions = useDimensions();
const safeArea = useSafeAreaInsets();
@@ -226,7 +150,17 @@ export const HoldersPlaceholder = memo(() => {
);
});
-export const HoldersLoader = memo(({ loaded, type }: { loaded: boolean, type: 'account' | 'create' | 'prepaid' }) => {
+export const HoldersLoader = memo(({
+ loaded,
+ type,
+ onReload,
+ onSupport
+}: {
+ loaded: boolean,
+ type: HoldersAppParamsType,
+ onReload?: () => void,
+ onSupport?: () => void
+}) => {
const theme = useTheme();
const navigation = useTypedNavigation();
const safeArea = useSafeAreaInsets();
@@ -237,9 +171,17 @@ export const HoldersLoader = memo(({ loaded, type }: { loaded: boolean, type: 'a
return { opacity: opacity.value };
});
+ const longLoadingTimerRef = useRef(null);
+ const start = useMemo(() => Date.now(), []);
+ const trackLoadingTime = useCallback(() => {
+ trackEvent(MixpanelEvent.HoldersLoadingTime, { type, duration: Date.now() - start });
+ }, []);
+
useEffect(() => {
if (loaded) {
+ longLoadingTimerRef.current && clearTimeout(longLoadingTimerRef.current);
opacity.value = withTiming(0, { duration: 350, easing: Easing.inOut(Easing.ease) });
+ trackLoadingTime();
} else {
setShowClose(false);
opacity.value = 1;
@@ -247,22 +189,42 @@ export const HoldersLoader = memo(({ loaded, type }: { loaded: boolean, type: 'a
}, [loaded]);
useEffect(() => {
- setTimeout(() => {
+ const showCloseTimer = setTimeout(() => {
setShowClose(true);
- }, 3000);
+ }, 7000);
+
+ if (longLoadingTimerRef.current) {
+ clearTimeout(longLoadingTimerRef.current);
+ }
+
+ longLoadingTimerRef.current = setTimeout(() => {
+ trackEvent(MixpanelEvent.holdersLongLoadingTime, { type, duration: 12000 });
+ }, 10000);
+
+ return () => {
+ longLoadingTimerRef.current && clearTimeout(longLoadingTimerRef.current);
+ clearTimeout(showCloseTimer);
+ }
}, []);
const placeholder = useMemo(() => {
- if (type === 'account') {
- return ;
+ if (type === HoldersAppParamsType.Account) {
+ return (
+
+ );
}
- if (type === 'prepaid') {
+ if (type === HoldersAppParamsType.Prepaid) {
return ;
}
return ;
- }, [type, theme]);
+ }, [type, theme, showClose]);
return (
[
+ {
+ opacity: pressed ? 0.5 : 1,
+ backgroundColor: type === HoldersAppParamsType.Account ? '#1c1c1e' : theme.surfaceOnBg,
+ borderRadius: 32,
+ height: 32, width: 32,
+ justifyContent: 'center', alignItems: 'center',
+ },
+ ]}
+ onPress={navigation.goBack}
+ >
+
+ : undefined}
style={[
{ position: 'absolute', top: 32, left: 16, right: 0 },
Platform.select({
@@ -317,6 +301,7 @@ export const HoldersAppComponent = memo((
const [currency,] = usePrimaryCurrency();
const selectedAccount = useSelectedAccount();
const url = holdersUrl(isTestnet);
+ const { showActionSheetWithOptions } = useActionSheet();
const source = useMemo(() => {
const queryParams = new URLSearchParams({
@@ -328,16 +313,19 @@ export const HoldersAppComponent = memo((
let route = '';
switch (props.variant.type) {
- case 'create':
+ case HoldersAppParamsType.Invite:
+ route = '/';
+ break;
+ case HoldersAppParamsType.Create:
route = '/create';
break;
- case 'account':
+ case HoldersAppParamsType.Account:
route = `/account/${props.variant.id}`;
break;
- case 'prepaid':
+ case HoldersAppParamsType.Prepaid:
route = `/card-prepaid/${props.variant.id}`;
break;
- case 'transactions':
+ case HoldersAppParamsType.Transactions:
route = `/transactions`;
for (const [key, value] of Object.entries(props.variant.query)) {
if (!!value) {
@@ -345,6 +333,14 @@ export const HoldersAppComponent = memo((
}
}
break;
+ case HoldersAppParamsType.Path:
+ route = `/${props.variant.path ?? ''}`;
+ for (const [key, value] of Object.entries(props.variant.query)) {
+ if (!!value) {
+ queryParams.append(key, value);
+ }
+ }
+ break;
}
const url = `${props.endpoint}${route}?${queryParams.toString()}`;
@@ -414,7 +410,7 @@ export const HoldersAppComponent = memo((
kycStatus: status.state === 'need-kyc' ? status.kycStatus : null,
suspended: (status as { suspended: boolean | undefined }).suspended === true,
},
- token: status.state === HoldersAccountState.Ok ? status.token : getHoldersToken(acc.address.toString({ testOnly: isTestnet })),
+ token: status.state === HoldersUserState.Ok ? status.token : getHoldersToken(acc.address.toString({ testOnly: isTestnet })),
}
}
: {},
@@ -477,8 +473,51 @@ export const HoldersAppComponent = memo((
injectSource
]);
+ const [renderKey, setRenderKey] = useState(0);
+
+ const onReaload = useCallback(() => {
+ setRenderKey(renderKey + 1);
+ }, []);
+
+ const onSupport = useCallback(() => {
+ const tonhubOptions = [
+ t('common.cancel'),
+ t('settings.support.telegram'),
+ t('settings.support.form'),
+ t('settings.support.holders')
+ ];
+ const cancelButtonIndex = 0;
+
+ const tonhubSupportSheet = () => {
+ showActionSheetWithOptions({
+ options: tonhubOptions,
+ title: t('settings.support.title'),
+ cancelButtonIndex,
+ }, (selectedIndex?: number) => {
+ switch (selectedIndex) {
+ case 1:
+ openWithInApp('https://t.me/WhalesSupportBot');
+ break;
+ case 2:
+ openWithInApp('https://airtable.com/appWErwfR8x0o7vmz/shr81d2H644BNUtPN');
+ break;
+ case 3:
+ openWithInApp('https://help.holders.io/en');
+ break;
+ default:
+ break;
+ }
+ });
+ }
+
+ tonhubSupportSheet();
+ }, []);
+
return (
-
+
(
)}
/>
diff --git a/app/fragments/secure/components/TransferSingleView.tsx b/app/fragments/secure/components/TransferSingleView.tsx
index b72dc4cc0..e52bbc94a 100644
--- a/app/fragments/secure/components/TransferSingleView.tsx
+++ b/app/fragments/secure/components/TransferSingleView.tsx
@@ -14,7 +14,7 @@ import { Address, fromNano, toNano } from "@ton/core";
import { WalletSettings } from "../../../engine/state/walletSettings";
import { useAppState, useNetwork, useBounceableWalletFormat, usePrice, useSelectedAccount, useTheme, useWalletsSettings, useVerifyJetton } from "../../../engine/hooks";
import { AddressComponent } from "../../../components/address/AddressComponent";
-import { holdersUrl as resolveHoldersUrl } from "../../../engine/api/holders/fetchAccountState";
+import { holdersUrl as resolveHoldersUrl } from "../../../engine/api/holders/fetchUserState";
import { useLedgerTransport } from "../../ledger/components/TransportContext";
import { Jetton, StoredOperation } from "../../../engine/types";
import { AboutIconButton } from "../../../components/AboutIconButton";
diff --git a/app/fragments/staking/LiquidStakingTransferFragment.tsx b/app/fragments/staking/LiquidStakingTransferFragment.tsx
index 3f099ba7a..41c6b4e13 100644
--- a/app/fragments/staking/LiquidStakingTransferFragment.tsx
+++ b/app/fragments/staking/LiquidStakingTransferFragment.tsx
@@ -31,43 +31,10 @@ import { Typography } from '../../components/styles';
import { useValidAmount } from '../../utils/useValidAmount';
import IcTonIcon from '@assets/ic-ton-acc.svg';
+import { LiquidStakingAmountAction, liquidStakingAmountReducer } from '../../utils/staking/liquidStakingAmountReducer';
export type LiquidStakingTransferParams = Omit;
-type AmountAction = { type: 'ton', amount: string } | { type: 'wsTon', amount: string };
-type AmountState = { ton: string, wsTon: string };
-
-function reduceAmountState(withdrawRate: bigint, depositRate: bigint, type: 'withdraw' | 'top_up') {
- return (state: AmountState, action: AmountAction): AmountState => {
- try {
- const amount = action.amount.replace(',', '.').replaceAll(' ', '');
- if (action.type === 'ton') {
- const ton = formatInputAmount(action.amount, 9, { skipFormattingDecimals: true }, state.ton);
- const computed = parseFloat(amount) * parseFloat(fromNano(type === 'withdraw' ? withdrawRate : depositRate)) || 0
- const wsTon = fromNano(toNano(computed.toFixed(9)));
-
- if (ton === state.ton) {
- return state;
- }
-
- return { ton, wsTon };
- }
-
- const wsTon = formatInputAmount(action.amount, 9, { skipFormattingDecimals: true }, state.wsTon);
- const computed = parseFloat(amount) * parseFloat(fromNano(type === 'withdraw' ? withdrawRate : depositRate)) || 0;
- const ton = fromNano(toNano(computed.toFixed(9)));
-
- if (wsTon === state.wsTon) {
- return state;
- }
-
- return { ton, wsTon };
- } catch {
- return state;
- }
- }
-}
-
export const LiquidStakingTransferFragment = fragment(() => {
const theme = useTheme();
const network = useNetwork();
@@ -101,7 +68,7 @@ export const LiquidStakingTransferFragment = fragment(() => {
}
if (params?.action === 'top_up' && params.amount) {
- initAmount = reduceAmountState(
+ initAmount = liquidStakingAmountReducer(
liquidStaking?.rateWithdraw ?? 0n,
liquidStaking?.rateDeposit ?? 0n,
'top_up'
@@ -109,7 +76,7 @@ export const LiquidStakingTransferFragment = fragment(() => {
}
const [amount, dispatchAmount] = useReducer(
- reduceAmountState(
+ liquidStakingAmountReducer(
liquidStaking?.rateWithdraw ?? 0n,
liquidStaking?.rateDeposit ?? 0n,
params?.action === 'withdraw' ? 'withdraw' : 'top_up'
@@ -131,7 +98,7 @@ export const LiquidStakingTransferFragment = fragment(() => {
return 0n;
}, [params.action, member, account]);
- const onSetAmount = useCallback((action: AmountAction) => {
+ const onSetAmount = useCallback((action: LiquidStakingAmountAction) => {
setMinAmountWarn(undefined);
dispatchAmount(action);
}, []);
diff --git a/app/fragments/wallet/ProductsFragment.tsx b/app/fragments/wallet/ProductsFragment.tsx
index 63728e1be..1a484c809 100644
--- a/app/fragments/wallet/ProductsFragment.tsx
+++ b/app/fragments/wallet/ProductsFragment.tsx
@@ -3,14 +3,15 @@ import { useCallback, useMemo } from "react";
import { fragment } from "../../fragment";
import { useTypedNavigation } from "../../utils/useTypedNavigation";
import { useHoldersAccountStatus, useIsConnectAppReady, useNetwork, useSelectedAccount, useStakingApy, useTheme } from "../../engine/hooks";
-import { HoldersAccountState, holdersUrl as resolveHoldersUrl } from "../../engine/api/holders/fetchAccountState";
+import { HoldersUserState, holdersUrl as resolveHoldersUrl } from "../../engine/api/holders/fetchUserState";
import { ScreenHeader } from "../../components/ScreenHeader";
import { t } from "../../i18n/t";
import { ProductBanner } from "../../components/products/ProductBanner";
import { StatusBar } from "expo-status-bar";
import { useSafeAreaInsets } from "react-native-safe-area-context";
-import { useIsHoldersWhitelisted } from "../../engine/hooks/holders/useIsHoldersWhitelisted";
+import { useIsHoldersInvited } from "../../engine/hooks/holders/useIsHoldersInvited";
import { Typography } from "../../components/styles";
+import { HoldersAppParamsType } from "../holders/HoldersAppFragment";
export const ProductsFragment = fragment(() => {
const navigation = useTypedNavigation();
@@ -22,7 +23,7 @@ export const ProductsFragment = fragment(() => {
const status = useHoldersAccountStatus(selected!.address).data;
const holdersUrl = resolveHoldersUrl(network.isTestnet);
const isHoldersReady = useIsConnectAppReady(holdersUrl);
- const isHoldersWhitelisted = useIsHoldersWhitelisted(selected!.address, network.isTestnet);
+ const isHoldersInvited = useIsHoldersInvited(selected!.address, network.isTestnet);
const apyWithFee = useMemo(() => {
if (!!apy) {
@@ -31,7 +32,7 @@ export const ProductsFragment = fragment(() => {
}, [apy]);
const needsEnrolment = useMemo(() => {
- if (status?.state === HoldersAccountState.NeedEnrollment) {
+ if (status?.state === HoldersUserState.NeedEnrollment) {
return true;
}
return false;
@@ -42,11 +43,11 @@ export const ProductsFragment = fragment(() => {
navigation.goBack();
if (needsEnrolment || !isHoldersReady) {
- navigation.navigateHoldersLanding({ endpoint: holdersUrl, onEnrollType: { type: 'create' } }, network.isTestnet);
+ navigation.navigateHoldersLanding({ endpoint: holdersUrl, onEnrollType: { type: HoldersAppParamsType.Create } }, network.isTestnet);
return;
}
- navigation.navigateHolders({ type: 'create' }, network.isTestnet);
+ navigation.navigateHolders({ type: HoldersAppParamsType.Create }, network.isTestnet);
}, [needsEnrolment, isHoldersReady, network.isTestnet]);
return (
@@ -81,7 +82,7 @@ export const ProductsFragment = fragment(() => {
{t('products.addNew')}
- {isHoldersWhitelisted && (
+ {isHoldersInvited && (
{
illustrationStyle={{ backgroundColor: theme.elevation }}
/>
)}
-
+
{
navigation.goBack();
diff --git a/app/i18n/i18n_en.ts b/app/i18n/i18n_en.ts
index 81e7ddab5..95a24446b 100644
--- a/app/i18n/i18n_en.ts
+++ b/app/i18n/i18n_en.ts
@@ -160,7 +160,8 @@ const schema: PrepareSchema = {
title: 'Receive',
subtitle: 'Only send TON Blockchain assets to this address. Other assets will be lost forever',
share: {
- title: 'My Tonhub Address'
+ title: 'My Tonhub Address',
+ error: 'Failed to share address, please try again or contact support'
}
},
transfer: {
@@ -501,6 +502,8 @@ const schema: PrepareSchema = {
},
holders: {
title: 'Bank account',
+ loadingLongerTitle: 'Connection problems',
+ loadingLonger: 'Check your internet connection and reload page. If the issue persists please contact support',
accounts: {
title: 'Payment accounts',
prepaidTitle: 'Prepaid cards',
diff --git a/app/i18n/i18n_ru.ts b/app/i18n/i18n_ru.ts
index c499f974d..4d271d3ce 100644
--- a/app/i18n/i18n_ru.ts
+++ b/app/i18n/i18n_ru.ts
@@ -144,7 +144,8 @@ const schema: PrepareSchema = {
"title": "Получить",
"subtitle": "Отправляйте на этот адрес только токены блокчейна TON. Другие активы будут потеряны навсегда",
"share": {
- "title": "My Tonhub Address"
+ "title": "Мой Tonhub адрес",
+ "error": "Не удалось поделиться адресом, попробуйте еще раз или обратитесь в службу поддержки"
}
},
"transfer": {
@@ -501,6 +502,8 @@ const schema: PrepareSchema = {
},
"holders": {
"title": "Банковский счет",
+ "loadingLongerTitle": "Проблемы c подключением",
+ "loadingLonger": "Проверьте подключение к интернету и перезагрузите страницу. Если проблема сохраняется, обратитесь в службу поддержки",
"accounts": {
"title": "Счета",
"prepaidTitle": 'Prepaid карты',
@@ -883,7 +886,7 @@ const schema: PrepareSchema = {
},
"delete": "Удалить контакт",
"empty": "У вас ещё нет контактов",
- "description": "Нажмите и подержите адрес, чтобы добавить его в контакты, или выберите его из списка недавних контактов ниже",
+ "description": "Нажмите и удерживайте адрес или выберите его из списка ниже, чтобы добавить в контакты",
"contactAddress": "Адрес контакта",
"search": "Имя или адрес кошелька",
"new": "Новый контакт"
diff --git a/app/i18n/schema.ts b/app/i18n/schema.ts
index 55b0d8986..5b6910b48 100644
--- a/app/i18n/schema.ts
+++ b/app/i18n/schema.ts
@@ -162,7 +162,8 @@ export type LocalizationSchema = {
title: string,
subtitle: string,
share: {
- title: string
+ title: string,
+ error: string
}
},
transfer: {
@@ -503,6 +504,8 @@ export type LocalizationSchema = {
},
holders: {
title: string,
+ loadingLongerTitle: string,
+ loadingLonger: string,
accounts: {
title: string,
prepaidTitle: string,
diff --git a/app/secure/KnownWallets.ts b/app/secure/KnownWallets.ts
index 0f58019e6..24f0eb9f7 100644
--- a/app/secure/KnownWallets.ts
+++ b/app/secure/KnownWallets.ts
@@ -32,6 +32,8 @@ const Img_venera = require('@assets/known/Img_venera.jpeg');
const Img_Team_1 = require('@assets/known/ic_team_1.png');
const Img_Team_2 = require('@assets/known/ic_team_2.png');
+const Img_Club_1 = require('@assets/ic_club_cosmos.png');
+const Img_Club_2 = require('@assets/ic_club_robot.png');
const Img_ePN_1 = require('@assets/known/ic_epn_1.png');
const Img_ePN_2 = require('@assets/known/ic_epn_2.png');
const Img_Lockups_1 = require('@assets/known/ic_lockups_1.png');
@@ -171,6 +173,24 @@ const knownWalletsMainnet = {
ic: Img_Team_2,
requireMemo: true
},
+ [Address.parse('EQDFvnxuyA2ogNPOoEj1lu968U4PP8_FzJfrOWUsi_o1CLUB').toString()]: {
+ name: 'Club 1',
+ colors: {
+ primary: '#65C6FF',
+ secondary: '#DEEFFC'
+ },
+ ic: Img_Club_1,
+ requireMemo: true
+ },
+ [Address.parse('EQA_cc5tIQ4haNbMVFUD1d0bNRt17S7wgWEqfP_xEaTACLUB').toString()]: {
+ name: 'Club 2',
+ colors: {
+ primary: '#65C6FF',
+ secondary: '#DEEFFC'
+ },
+ ic: Img_Club_2,
+ requireMemo: true
+ },
[Address.parse('EQBeNwQShukLyOWjKWZ0Oxoe5U3ET-ApQIWYeC4VLZ4tmeTm').toString()]: {
name: 'Whales Pool Withdraw 1',
colors: {
@@ -489,11 +509,52 @@ const knownWalletsMainnet = {
ic: Img_OKX,
requireMemo: true
},
+ [Address.parse('EQCbm3Od4lJ65y0hCD9RmNcQxyiEU7RzPlfsrbLhiayCnNuU').toString()]: {
+ name: 'OKX',
+ ic: Img_OKX,
+ requireMemo: true
+ },
[Address.parse('EQCFTsRSHv1SrUO88ZiOTETr35omrRj6Uav9toX8OzSKXGkS').toString()]: {
name: 'OKX',
ic: Img_OKX,
requireMemo: true
},
+ [Address.parse('EQDxXbLGLNhq_whg05xJH8c6MTlfr-tZReZESYViJgx4Lg4_').toString()]: {
+ name: 'OKX',
+ ic: Img_OKX,
+ requireMemo: true
+ },
+ [Address.parse('UQDn6G2gh0LtkzQ0_-uPCKY8fhAO6ELiX1manL8IkVdKbDEu').toString()]: {
+ name: 'OKX',
+ ic: Img_OKX,
+ requireMemo: true
+ },
+ [Address.parse('UQADON7zS4TG7pE0oEqxYBRQvkRjQKN64lneV8s3vbWQzTjL').toString()]: {
+ name: 'OKX',
+ ic: Img_OKX,
+ requireMemo: true
+ },
+ [Address.parse('UQCjCknscl6fVyRLZq9MLaerdgBLT86A06NLHNDDd2Krztab').toString()]: {
+ name: 'OKX',
+ ic: Img_OKX,
+ requireMemo: true
+ },
+ [Address.parse('UQAk2h57jakB5bPdD5jDl_625aXqSDGVqLa9BoLPLFMnrnbQ').toString()]: {
+ name: 'OKX',
+ ic: Img_OKX,
+ requireMemo: true
+ },
+ [Address.parse('EQB_LoTHI9i2trGqz4EMjCarI8IgIrRkuHEEstMGWw6nC3Nw').toString()]: {
+ name: 'OKX',
+ ic: Img_OKX,
+ requireMemo: true
+ },
+ [Address.parse('EQD5vcDeRhwaLgAvralVC7sJXI-fc2aNcMUXqcx-BQ-OWnOZ').toString()]: {
+ name: 'OKX',
+ ic: Img_OKX,
+ requireMemo: true
+ },
+
[Address.parse('EQABMMdzRuntgt9nfRB61qd1wR-cGPagXA3ReQazVYUNrT7p').toString()]: {
name: 'EXMO Deposit',
ic: Img_EXMO_Deposit,
diff --git a/app/useLinkNavigator.ts b/app/useLinkNavigator.ts
index 5935c3849..11b13d25e 100644
--- a/app/useLinkNavigator.ts
+++ b/app/useLinkNavigator.ts
@@ -32,9 +32,9 @@ import { extractDomain } from './engine/utils/extractDomain';
import { Linking } from 'react-native';
import { openWithInApp } from './utils/openWithInApp';
import { getHoldersToken, HoldersAccountStatus } from './engine/hooks/holders/useHoldersAccountStatus';
-import { HoldersAccountState, holdersUrl } from './engine/api/holders/fetchAccountState';
+import { HoldersUserState, holdersUrl } from './engine/api/holders/fetchUserState';
import { getIsConnectAppReady } from './engine/hooks/dapps/useIsConnectAppReady';
-import { HoldersAppParams } from './fragments/holders/HoldersAppFragment';
+import { HoldersAppParams, HoldersAppParamsType } from './fragments/holders/HoldersAppFragment';
const infoBackoff = createBackoff({ maxFailureCount: 10 });
@@ -473,22 +473,27 @@ function getNeedsEnrollment(url: string, address: string, isTestnet: boolean, qu
return true;
}
- if (status.state === HoldersAccountState.NeedEnrollment) {
+ if (status.state === HoldersUserState.NeedEnrollment) {
return true;
}
return false;
}
-function resolveAndNavigateToHolders(params: {
+type HolderResloveParams = {
query: { [key: string]: string | undefined },
navigation: TypedNavigation,
selected: SelectedAccount,
updateAppState: (value: AppState, isTestnet: boolean) => void,
isTestnet: boolean,
queryClient: QueryClient
-}) {
- const { query, navigation, selected, updateAppState, queryClient, isTestnet } = params
+}
+
+type HoldersTransactionResolveParams = HolderResloveParams & { type: 'holders-transactions' }
+type HoldersPathResolveParams = HolderResloveParams & { type: 'holders-path', path: string }
+
+function resolveAndNavigateToHolders(params: HoldersTransactionResolveParams | HoldersPathResolveParams) {
+ const { type, query, navigation, selected, updateAppState, queryClient, isTestnet } = params
const addresses = query['addresses']?.split(',');
if (!addresses || addresses.length === 0) {
@@ -498,10 +503,15 @@ function resolveAndNavigateToHolders(params: {
const isSelectedAddress = addresses.find((a) => Address.parse(a).equals(selected.address));
const transactionId = query['transactionId'];
- const holdersNavParams: HoldersAppParams = {
- type: 'transactions',
- query: { transactionId }
- }
+ const holdersNavParams: HoldersAppParams = type === 'holders-transactions'
+ ? {
+ type: HoldersAppParamsType.Transactions,
+ query: { transactionId }
+ } : {
+ type: HoldersAppParamsType.Path,
+ path: params.path,
+ query
+ }
const url = holdersUrl(isTestnet);
@@ -552,6 +562,18 @@ function resolveAndNavigateToHolders(params: {
}
}
+function resolveHoldersInviteLink(params: {
+ navigation: TypedNavigation,
+ isTestnet: boolean,
+ inviteId: string
+}) {
+ const { navigation, isTestnet, inviteId } = params
+
+ const endpoint = holdersUrl(isTestnet);
+
+ navigation.navigateHoldersLanding({ endpoint, onEnrollType: { type: HoldersAppParamsType.Invite }, inviteId }, isTestnet);
+}
+
export function useLinkNavigator(
isTestnet: boolean,
toastProps?: { duration?: ToastDuration, marginBottom?: number },
@@ -686,6 +708,24 @@ export function useLinkNavigator(
}
resolveAndNavigateToHolders({
+ type: 'holders-transactions',
+ navigation,
+ query: resolved.query,
+ selected,
+ updateAppState,
+ isTestnet,
+ queryClient
+ });
+ break;
+ }
+ case 'holders-path': {
+ if (!selected) {
+ return;
+ }
+
+ resolveAndNavigateToHolders({
+ type: 'holders-path',
+ path: resolved.path,
navigation,
query: resolved.query,
selected,
@@ -693,6 +733,19 @@ export function useLinkNavigator(
isTestnet,
queryClient
});
+ break;
+ }
+ case 'holders-invite': {
+ if (!selected) {
+ return;
+ }
+
+ resolveHoldersInviteLink({
+ navigation,
+ isTestnet,
+ inviteId: resolved.inviteId
+ })
+ break;
}
}
diff --git a/app/utils/resolveUrl.ts b/app/utils/resolveUrl.ts
index 141634735..a60fa2fb1 100644
--- a/app/utils/resolveUrl.ts
+++ b/app/utils/resolveUrl.ts
@@ -68,6 +68,13 @@ export type ResolvedUrl = {
} | {
type: 'holders-transactions',
query: { [key: string]: string | undefined }
+} | {
+ type: 'holders-path',
+ path: string,
+ query: { [key: string]: string | undefined }
+} | {
+ type: 'holders-invite',
+ inviteId: string
}
export function isUrl(str: string): boolean {
@@ -87,6 +94,25 @@ function resolveHoldersUrl(url: Url>): Resolv
type: 'holders-transactions',
query: url.query
}
+ } else if (!!url.query && url.query.path) {
+ const path = decodeURIComponent(url.query.path);
+ delete url.query.path;
+
+ return {
+ type: 'holders-path',
+ path,
+ query: url.query
+ }
+ }
+
+ const isInvite = url.pathname.startsWith('/holders/invite');
+ const inviteId = url.pathname.split('holders/invite/')[1]
+
+ if (isInvite && inviteId) {
+ return {
+ type: 'holders-invite',
+ inviteId: inviteId
+ }
}
return { type: 'error', error: ResolveUrlError.InvalidHoldersPath };
diff --git a/app/utils/staking/liquidStakingAmountReducer.ts b/app/utils/staking/liquidStakingAmountReducer.ts
new file mode 100644
index 000000000..36d09430c
--- /dev/null
+++ b/app/utils/staking/liquidStakingAmountReducer.ts
@@ -0,0 +1,41 @@
+import { fromNano, toNano } from "@ton/core";
+import { formatInputAmount } from "../formatCurrency";
+
+export type LiquidStakingAmountAction = { type: 'ton', amount: string } | { type: 'wsTon', amount: string };
+type AmountState = { ton: string, wsTon: string };
+
+export function liquidStakingAmountReducer(withdrawRate: bigint, depositRate: bigint, type: 'withdraw' | 'top_up') {
+ return (state: AmountState, action: LiquidStakingAmountAction): AmountState => {
+ if (action.amount === '' || action.amount === '') {
+ return { ton: '', wsTon: '' };
+ }
+ try {
+ const rate = fromNano(type === 'withdraw' ? withdrawRate : depositRate);
+
+ const amount = action.amount.replace(',', '.').replaceAll(' ', '');
+ if (action.type === 'ton') {
+ const ton = formatInputAmount(action.amount, 9, { skipFormattingDecimals: true }, state.ton);
+ const computed = parseFloat(amount) * (1 / parseFloat(rate));
+ const wsTon = fromNano(toNano(computed.toFixed(9)));
+
+ if (ton === state.ton) {
+ return state;
+ }
+
+ return { ton, wsTon };
+ }
+
+ const wsTon = formatInputAmount(action.amount, 9, { skipFormattingDecimals: true }, state.wsTon);
+ const computed = parseFloat(amount) * parseFloat(rate);
+ const ton = fromNano(toNano(computed.toFixed(9)));
+
+ if (wsTon === state.wsTon) {
+ return state;
+ }
+
+ return { ton, wsTon };
+ } catch {
+ return state;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/utils/useTypedNavigation.ts b/app/utils/useTypedNavigation.ts
index 7d8dc612e..9d69728d9 100644
--- a/app/utils/useTypedNavigation.ts
+++ b/app/utils/useTypedNavigation.ts
@@ -167,16 +167,16 @@ export class TypedNavigation {
this.navigateAndReplaceAll('LedgerApp');
}
- navigateHoldersLanding({ endpoint, onEnrollType }: { endpoint: string, onEnrollType: HoldersAppParams }, isTestnet: boolean) {
+ navigateHoldersLanding({ endpoint, onEnrollType, inviteId }: { endpoint: string, onEnrollType: HoldersAppParams, inviteId?: string }, isTestnet: boolean) {
if (shouldTurnAuthOn(isTestnet)) {
const callback = (success: boolean) => {
if (success) { // navigate only if auth is set up
- this.navigate('HoldersLanding', { endpoint, onEnrollType })
+ this.navigate('HoldersLanding', { endpoint, onEnrollType, inviteId })
}
}
this.navigateMandatoryAuthSetup({ callback });
} else {
- this.navigate('HoldersLanding', { endpoint, onEnrollType });
+ this.navigate('HoldersLanding', { endpoint, onEnrollType, inviteId });
}
}
diff --git a/assets/ic-bad-connection.png b/assets/ic-bad-connection.png
new file mode 100644
index 000000000..f8fb93747
Binary files /dev/null and b/assets/ic-bad-connection.png differ
diff --git a/assets/ic-bad-connection@2x.png b/assets/ic-bad-connection@2x.png
new file mode 100644
index 000000000..3d3cdca1a
Binary files /dev/null and b/assets/ic-bad-connection@2x.png differ
diff --git a/assets/ic-bad-connection@3x.png b/assets/ic-bad-connection@3x.png
new file mode 100644
index 000000000..9ed9f9a6a
Binary files /dev/null and b/assets/ic-bad-connection@3x.png differ
diff --git a/assets/ic-comment.png b/assets/ic-comment.png
new file mode 100644
index 000000000..29c475f06
Binary files /dev/null and b/assets/ic-comment.png differ
diff --git a/assets/ic-comment@2x.png b/assets/ic-comment@2x.png
new file mode 100644
index 000000000..16843a5b6
Binary files /dev/null and b/assets/ic-comment@2x.png differ
diff --git a/assets/ic-comment@3x.png b/assets/ic-comment@3x.png
new file mode 100644
index 000000000..979f52e87
Binary files /dev/null and b/assets/ic-comment@3x.png differ
diff --git a/assets/ic-reload.png b/assets/ic-reload.png
new file mode 100644
index 000000000..a7118a594
Binary files /dev/null and b/assets/ic-reload.png differ
diff --git a/assets/ic-reload@2x.png b/assets/ic-reload@2x.png
new file mode 100644
index 000000000..d11603415
Binary files /dev/null and b/assets/ic-reload@2x.png differ
diff --git a/assets/ic-reload@3x.png b/assets/ic-reload@3x.png
new file mode 100644
index 000000000..9475da8e1
Binary files /dev/null and b/assets/ic-reload@3x.png differ
diff --git a/ios/ci_scripts/ci_post_clone.sh b/ios/ci_scripts/ci_post_clone.sh
index 6e738cca6..bfee41934 100644
--- a/ios/ci_scripts/ci_post_clone.sh
+++ b/ios/ci_scripts/ci_post_clone.sh
@@ -9,10 +9,12 @@ export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=TRUE
# brew doesn't have version pinning for cocoapods, so we have to install it manually from the commit
echo "===== Uninstalling prev cocoapods ====="
brew uninstall --ignore-dependencies cocoapods || true
-echo "===== Downloading formula ====="
-curl https://raw.githubusercontent.com/Homebrew/homebrew-core/1364b74ebeedb2eab300d62c99e12f2a6f344277/Formula/c/cocoapods.rb > cocoapods.rb
-echo "===== Installing formula ====="
-brew install cocoapods.rb
+# echo "===== Downloading formula ====="
+# curl https://raw.githubusercontent.com/Homebrew/homebrew-core/1364b74ebeedb2eab300d62c99e12f2a6f344277/Formula/c/cocoapods.rb > cocoapods.rb
+# echo "===== Installing formula ====="
+# brew install cocoapods.rb
+echo "===== Installing cocoapods====="
+brew install cocoapods
echo "===== Installing node ====="
# have to add node yourself
diff --git a/ios/wallet/Info.plist b/ios/wallet/Info.plist
index 9f1def4a8..565dbc4f1 100644
--- a/ios/wallet/Info.plist
+++ b/ios/wallet/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- 2.3.12
+ 2.3.13
CFBundleSignature
????
CFBundleURLTypes
@@ -41,7 +41,7 @@
CFBundleVersion
- 206
+ 207
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/package.json b/package.json
index efda4eca8..35e822038 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "wallet",
- "version": "2.3.12",
+ "version": "2.3.13",
"scripts": {
"start": "expo start --dev-client",
"android": "expo run:android",
@@ -124,7 +124,7 @@
"react-native-reanimated": "^3.6.1",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
- "react-native-share": "^9.4.1",
+ "react-native-share": "^10.2.1",
"react-native-sse": "^1.1.0",
"react-native-svg": "14.0.0",
"react-native-tab-view": "^3.5.2",
diff --git a/yarn.lock b/yarn.lock
index 9640e378e..237721844 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9195,10 +9195,10 @@ react-native-screens@~3.22.0:
react-freeze "^1.0.0"
warn-once "^0.1.0"
-react-native-share@^9.4.1:
- version "9.4.1"
- resolved "https://registry.yarnpkg.com/react-native-share/-/react-native-share-9.4.1.tgz#1b6d96015009e3878bfc4346940602c1cffff525"
- integrity sha512-jm4qA5J5+ytWA8UFg6s8iEfdZYGPW+t5oreSuzrPt0assjvBUlFaoqYGGwGR5RJ8BIpjzOJYvx/c9MjXB4ApUg==
+react-native-share@^10.2.1:
+ version "10.2.1"
+ resolved "https://registry.yarnpkg.com/react-native-share/-/react-native-share-10.2.1.tgz#baf94848c2acee6e52f6b28e05c47fa5fa9402be"
+ integrity sha512-Z2LWGYWH7raM4H6Oauttv1tEhaB43XSWJAN8iS6oaSG9CnyrUBeYFF4QpU1AH5RgNeylXQdN8CtbizCHHt6coQ==
react-native-sse@^1.1.0:
version "1.2.0"