@rango-dev/widget-embedded 0.20.1-next.21 → 0.20.1-next.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/QueueManager.d.ts.map +1 -1
  2. package/dist/components/AppRouter.d.ts.map +1 -1
  3. package/dist/components/AppRoutes.d.ts.map +1 -1
  4. package/dist/components/ConfirmWalletsModal/ConfirmWallets.styles.d.ts +3 -3
  5. package/dist/components/ConfirmWalletsModal/WalletList.d.ts.map +1 -1
  6. package/dist/components/Layout/Layout.d.ts.map +1 -1
  7. package/dist/components/NotificationContent/NotificationContent.d.ts.map +1 -1
  8. package/dist/components/Quote/Quote.styles.d.ts +2 -2
  9. package/dist/components/QuoteWarningsAndErrors/HighValueLossWarningModal.d.ts +1 -0
  10. package/dist/components/QuoteWarningsAndErrors/HighValueLossWarningModal.d.ts.map +1 -1
  11. package/dist/components/QuoteWarningsAndErrors/QuoteWarningsAndErrors.d.ts.map +1 -1
  12. package/dist/components/QuoteWarningsAndErrors/QuoteWarningsAndErrors.types.d.ts +1 -0
  13. package/dist/components/QuoteWarningsAndErrors/QuoteWarningsAndErrors.types.d.ts.map +1 -1
  14. package/dist/components/QuoteWarningsAndErrors/SlippageWariningModal.d.ts +1 -0
  15. package/dist/components/QuoteWarningsAndErrors/SlippageWariningModal.d.ts.map +1 -1
  16. package/dist/components/QuoteWarningsAndErrors/UnknownPriceWarningModal.d.ts +1 -0
  17. package/dist/components/QuoteWarningsAndErrors/UnknownPriceWarningModal.d.ts.map +1 -1
  18. package/dist/components/SwapDetails/SwapDetails.d.ts.map +1 -1
  19. package/dist/components/SwapDetailsModal/SwapDetailsModal.WalletState.d.ts.map +1 -1
  20. package/dist/components/SwapDetailsModal/SwapDetailsModal.d.ts.map +1 -1
  21. package/dist/components/SwapDetailsModal/SwapDetailsModal.types.d.ts +2 -0
  22. package/dist/components/SwapDetailsModal/SwapDetailsModal.types.d.ts.map +1 -1
  23. package/dist/components/common/ActivateTabAlert/ActivateTabAlert.d.ts +4 -0
  24. package/dist/components/common/ActivateTabAlert/ActivateTabAlert.d.ts.map +1 -0
  25. package/dist/components/common/ActivateTabAlert/ActivateTabAlert.types.d.ts +4 -0
  26. package/dist/components/common/ActivateTabAlert/ActivateTabAlert.types.d.ts.map +1 -0
  27. package/dist/components/common/ActivateTabAlert/index.d.ts +2 -0
  28. package/dist/components/common/ActivateTabAlert/index.d.ts.map +1 -0
  29. package/dist/components/common/ActivateTabModal/ActivateTabModal.d.ts +4 -0
  30. package/dist/components/common/ActivateTabModal/ActivateTabModal.d.ts.map +1 -0
  31. package/dist/components/common/ActivateTabModal/ActivateTabModal.types.d.ts +6 -0
  32. package/dist/components/common/ActivateTabModal/ActivateTabModal.types.d.ts.map +1 -0
  33. package/dist/components/common/ActivateTabModal/index.d.ts +2 -0
  34. package/dist/components/common/ActivateTabModal/index.d.ts.map +1 -0
  35. package/dist/constants/searchParams.d.ts +6 -6
  36. package/dist/constants/searchParams.d.ts.map +1 -1
  37. package/dist/containers/Widget/Widget.d.ts.map +1 -1
  38. package/dist/containers/WidgetInfo/WidgetInfo.d.ts.map +1 -1
  39. package/dist/containers/WidgetInfo/WidgetInfo.types.d.ts +2 -0
  40. package/dist/containers/WidgetInfo/WidgetInfo.types.d.ts.map +1 -1
  41. package/dist/hooks/useSwapInput.d.ts.map +1 -1
  42. package/dist/hooks/useSyncStoresWithConfig.d.ts.map +1 -1
  43. package/dist/hooks/useSyncUrlAndStore/index.d.ts +2 -0
  44. package/dist/hooks/useSyncUrlAndStore/index.d.ts.map +1 -0
  45. package/dist/hooks/useSyncUrlAndStore/useSyncUrlAndStore.d.ts +2 -0
  46. package/dist/hooks/useSyncUrlAndStore/useSyncUrlAndStore.d.ts.map +1 -0
  47. package/dist/hooks/useSyncUrlAndStore/useSyncUrlAndStore.helpers.d.ts +4 -0
  48. package/dist/hooks/useSyncUrlAndStore/useSyncUrlAndStore.helpers.d.ts.map +1 -0
  49. package/dist/index.js +2 -2
  50. package/dist/index.js.map +4 -4
  51. package/dist/libs/tabManager/index.d.ts +3 -0
  52. package/dist/libs/tabManager/index.d.ts.map +1 -0
  53. package/dist/libs/tabManager/tabManager.constants.d.ts +4 -0
  54. package/dist/libs/tabManager/tabManager.constants.d.ts.map +1 -0
  55. package/dist/libs/tabManager/tabManager.d.ts +63 -0
  56. package/dist/libs/tabManager/tabManager.d.ts.map +1 -0
  57. package/dist/libs/tabManager/tabManager.types.d.ts +29 -0
  58. package/dist/libs/tabManager/tabManager.types.d.ts.map +1 -0
  59. package/dist/pages/ConfirmSwapPage.d.ts.map +1 -1
  60. package/dist/pages/HistoryPage.d.ts.map +1 -1
  61. package/dist/pages/Home.d.ts.map +1 -1
  62. package/dist/pages/SwapDetailsPage.d.ts.map +1 -1
  63. package/dist/pages/WalletsPage.d.ts.map +1 -1
  64. package/dist/store/ui.d.ts +12 -10
  65. package/dist/store/ui.d.ts.map +1 -1
  66. package/dist/utils/quote.d.ts +0 -1
  67. package/dist/utils/quote.d.ts.map +1 -1
  68. package/package.json +4 -4
  69. package/src/QueueManager.tsx +4 -1
  70. package/src/components/AppRouter.tsx +1 -8
  71. package/src/components/AppRoutes.tsx +11 -0
  72. package/src/components/ConfirmWalletsModal/WalletList.tsx +3 -0
  73. package/src/components/Layout/Layout.tsx +37 -8
  74. package/src/components/NotificationContent/NotificationContent.tsx +0 -3
  75. package/src/components/QuoteWarningsAndErrors/HighValueLossWarningModal.tsx +3 -1
  76. package/src/components/QuoteWarningsAndErrors/QuoteWarningsAndErrors.tsx +3 -1
  77. package/src/components/QuoteWarningsAndErrors/QuoteWarningsAndErrors.types.ts +1 -0
  78. package/src/components/QuoteWarningsAndErrors/SlippageWariningModal.tsx +3 -1
  79. package/src/components/QuoteWarningsAndErrors/UnknownPriceWarningModal.tsx +3 -1
  80. package/src/components/SwapDetails/SwapDetails.tsx +3 -0
  81. package/src/components/SwapDetailsModal/SwapDetailsModal.WalletState.tsx +9 -1
  82. package/src/components/SwapDetailsModal/SwapDetailsModal.tsx +10 -2
  83. package/src/components/SwapDetailsModal/SwapDetailsModal.types.ts +2 -0
  84. package/src/components/common/ActivateTabAlert/ActivateTabAlert.tsx +26 -0
  85. package/src/components/common/ActivateTabAlert/ActivateTabAlert.types.ts +3 -0
  86. package/src/components/common/ActivateTabAlert/index.ts +1 -0
  87. package/src/components/common/ActivateTabModal/ActivateTabModal.tsx +31 -0
  88. package/src/components/common/ActivateTabModal/ActivateTabModal.types.ts +5 -0
  89. package/src/components/common/ActivateTabModal/index.ts +1 -0
  90. package/src/constants/searchParams.ts +6 -6
  91. package/src/containers/Wallets/Wallets.tsx +3 -3
  92. package/src/containers/Widget/Widget.tsx +6 -1
  93. package/src/containers/WidgetInfo/WidgetInfo.tsx +5 -1
  94. package/src/containers/WidgetInfo/WidgetInfo.types.ts +2 -0
  95. package/src/hooks/useSwapInput.ts +6 -1
  96. package/src/hooks/useSyncStoresWithConfig.ts +3 -17
  97. package/src/hooks/useSyncUrlAndStore/index.ts +1 -0
  98. package/src/hooks/useSyncUrlAndStore/useSyncUrlAndStore.helpers.ts +35 -0
  99. package/src/hooks/useSyncUrlAndStore/useSyncUrlAndStore.ts +135 -0
  100. package/src/libs/tabManager/index.ts +2 -0
  101. package/src/libs/tabManager/tabManager.constants.ts +3 -0
  102. package/src/libs/tabManager/tabManager.ts +222 -0
  103. package/src/libs/tabManager/tabManager.types.ts +28 -0
  104. package/src/pages/ConfirmSwapPage.tsx +6 -5
  105. package/src/pages/HistoryPage.tsx +1 -6
  106. package/src/pages/Home.tsx +12 -6
  107. package/src/pages/SwapDetailsPage.tsx +21 -7
  108. package/src/pages/WalletsPage.tsx +3 -0
  109. package/src/store/ui.ts +39 -11
  110. package/src/utils/quote.ts +0 -27
  111. package/dist/components/UpdateUrl.d.ts +0 -2
  112. package/dist/components/UpdateUrl.d.ts.map +0 -1
  113. package/src/components/UpdateUrl.tsx +0 -158
@@ -0,0 +1,26 @@
1
+ import type { PropTypes } from './ActivateTabAlert.types';
2
+
3
+ import { i18n } from '@lingui/core';
4
+ import { Alert, Button } from '@rango-dev/ui';
5
+ import React from 'react';
6
+
7
+ export function ActivateTabAlert(props: PropTypes) {
8
+ return (
9
+ <Alert
10
+ action={
11
+ <Button
12
+ onClick={props.onActivateTab}
13
+ variant="contained"
14
+ size="xxsmall"
15
+ type="warning">
16
+ {i18n.t('Activate')}
17
+ </Button>
18
+ }
19
+ type="warning"
20
+ variant="alarm"
21
+ title={i18n.t(
22
+ 'Another tab is open and handles transactions. You can activate this tab.'
23
+ )}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,3 @@
1
+ export type PropTypes = {
2
+ onActivateTab: () => void;
3
+ };
@@ -0,0 +1 @@
1
+ export { ActivateTabAlert } from './ActivateTabAlert';
@@ -0,0 +1,31 @@
1
+ import type { PropTypes } from './ActivateTabModal.types';
2
+
3
+ import { i18n } from '@lingui/core';
4
+ import { Button, Divider, MessageBox, Modal } from '@rango-dev/ui';
5
+ import React from 'react';
6
+
7
+ import { getContainer } from '../../../utils/common';
8
+
9
+ export function ActivateTabModal(props: PropTypes) {
10
+ const { open, onClose, onConfirm } = props;
11
+ return (
12
+ <Modal open={open} dismissible onClose={onClose} container={getContainer()}>
13
+ <MessageBox
14
+ title={i18n.t('Activate current tab')}
15
+ type="warning"
16
+ description={i18n.t(
17
+ 'Currently, some transactions are running and being handled by other browser tab. If you activate this tab, all transactions that are already in the transaction sign step will expire.'
18
+ )}>
19
+ <Divider size={20} />
20
+ <Button
21
+ variant="contained"
22
+ size="large"
23
+ type="primary"
24
+ fullWidth
25
+ onClick={onConfirm}>
26
+ {i18n.t('Confirm')}
27
+ </Button>
28
+ </MessageBox>
29
+ </Modal>
30
+ );
31
+ }
@@ -0,0 +1,5 @@
1
+ export type PropTypes = {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ onConfirm: () => void;
5
+ };
@@ -0,0 +1 @@
1
+ export { ActivateTabModal } from './ActivateTabModal';
@@ -1,8 +1,8 @@
1
1
  export enum SearchParams {
2
- 'FROM_CHAIN' = 'fromBlockchain',
3
- 'FROM_TOKEN' = 'fromToken',
4
- 'TO_CHAIN' = 'toBlockchain',
5
- 'TO_TOKEN' = 'toToken',
6
- 'FROM_AMOUNT' = 'fromAmount',
7
- 'AUTO_CONNECT' = 'autoConnect',
2
+ FROM_BLOCKCHAIN = 'fromBlockchain',
3
+ FROM_TOKEN = 'fromToken',
4
+ TO_BLOCKCHAIN = 'toBlockchain',
5
+ TO_TOKEN = 'toToken',
6
+ FROM_AMOUNT = 'fromAmount',
7
+ AUTO_CONNECT = 'autoConnect',
8
8
  }
@@ -13,9 +13,9 @@ import { Events, Provider } from '@rango-dev/wallets-react';
13
13
  import { isEvmBlockchain } from 'rango-sdk';
14
14
  import React, { createContext, useEffect, useRef, useState } from 'react';
15
15
 
16
- import { useSyncStoresWithConfig } from '../../hooks/useSyncStoresWithConfig';
17
16
  import { useWalletProviders } from '../../hooks/useWalletProviders';
18
17
  import { AppStoreProvider, useAppStore } from '../../store/AppStore';
18
+ import { useUiStore } from '../../store/ui';
19
19
  import { useWalletsStore } from '../../store/wallets';
20
20
  import {
21
21
  prepareAccountsForWalletStore,
@@ -39,7 +39,6 @@ function Main(props: PropsWithChildren<PropTypes>) {
39
39
  const { providers } = useWalletProviders(props.config.wallets, walletOptions);
40
40
  const { connectWallet, disconnectWallet } = useWalletsStore();
41
41
  const onConnectWalletHandler = useRef<OnConnectHandler>();
42
- useSyncStoresWithConfig();
43
42
 
44
43
  useEffect(() => {
45
44
  void fetchMeta().catch(console.log);
@@ -101,6 +100,7 @@ function Main(props: PropsWithChildren<PropTypes>) {
101
100
  props.onUpdateState(type, event, value, state, meta);
102
101
  }
103
102
  };
103
+ const isActiveTab = useUiStore.use.isActiveTab();
104
104
 
105
105
  useEffect(() => {
106
106
  if (accounts.length) {
@@ -120,7 +120,7 @@ function Main(props: PropsWithChildren<PropTypes>) {
120
120
  allBlockChains={blockchains}
121
121
  providers={providers}
122
122
  onUpdateState={onUpdateState}
123
- autoConnect>
123
+ autoConnect={!!isActiveTab}>
124
124
  {props.children}
125
125
  </Provider>
126
126
  </WidgetContext.Provider>
@@ -1,13 +1,18 @@
1
1
  import type { WidgetProps } from './Widget.types';
2
2
  import type { PropsWithChildren } from 'react';
3
3
 
4
- import React from 'react';
4
+ import React, { useEffect } from 'react';
5
5
 
6
6
  import { DEFAULT_CONFIG } from '../../store/slices/config';
7
+ import { destroyTabManager } from '../../store/ui';
7
8
  import { Main } from '../App';
8
9
  import { WidgetProvider } from '../WidgetProvider';
9
10
 
10
11
  export function Widget(props: PropsWithChildren<WidgetProps>) {
12
+ useEffect(() => {
13
+ return destroyTabManager;
14
+ }, []);
15
+
11
16
  if (!props.config?.externalWallets) {
12
17
  return (
13
18
  <WidgetProvider config={props.config ?? DEFAULT_CONFIG}>
@@ -7,6 +7,7 @@ import { useLanguage } from '../../hooks/useLanguage';
7
7
  import { useAppStore } from '../../store/AppStore';
8
8
  import { useNotificationStore } from '../../store/notification';
9
9
  import { useQuoteStore } from '../../store/quote';
10
+ import { setCurrentTabAsActive, useUiStore } from '../../store/ui';
10
11
  import { useWalletsStore } from '../../store/wallets';
11
12
  import { calculateWalletUsdValue } from '../../utils/wallets';
12
13
 
@@ -18,6 +19,7 @@ export const WidgetInfoContext = createContext<
18
19
 
19
20
  export function WidgetInfo(props: React.PropsWithChildren) {
20
21
  const { manager } = useManager();
22
+ const isActiveTab = useUiStore.use.isActiveTab();
21
23
  const retrySwap = useQuoteStore.use.retry();
22
24
  const history = new WidgetHistory(manager, { retrySwap });
23
25
  const details = useWalletsStore.use.connectedWallets();
@@ -32,7 +34,9 @@ export function WidgetInfo(props: React.PropsWithChildren) {
32
34
  const notifications = useNotificationStore().getUnreadNotifications();
33
35
 
34
36
  // eslint-disable-next-line react/jsx-no-constructed-context-values
35
- const value = {
37
+ const value: WidgetInfoContextInterface = {
38
+ isActiveTab,
39
+ setCurrentTabAsActive,
36
40
  history,
37
41
  wallets: {
38
42
  isLoading,
@@ -6,6 +6,8 @@ import type { Notification } from '../../types/notification';
6
6
  import type { BlockchainMeta, SwapperMeta, Token } from 'rango-sdk';
7
7
 
8
8
  export interface WidgetInfoContextInterface {
9
+ isActiveTab: boolean;
10
+ setCurrentTabAsActive: () => void;
9
11
  history: WidgetHistory;
10
12
  wallets: {
11
13
  details: ConnectedWallet[];
@@ -59,8 +59,9 @@ export function useSwapInput(): UseSwapInput {
59
59
  affiliateRef,
60
60
  affiliateWallets,
61
61
  disabledLiquiditySources,
62
+ fetchStatus,
62
63
  } = useAppStore();
63
- const [loading, setLoading] = useState(false);
64
+ const [loading, setLoading] = useState(true);
64
65
  const [error, setError] = useState<QuoteError | null>(null);
65
66
  const [warning, setWarning] = useState<QuoteWarning | null>(null);
66
67
  const userSlippage = customSlippage ?? slippage;
@@ -148,6 +149,9 @@ export function useSwapInput(): UseSwapInput {
148
149
  );
149
150
 
150
151
  useEffect(() => {
152
+ if (fetchStatus !== 'success') {
153
+ return;
154
+ }
151
155
  if (!isPositiveNumber(inputAmount) || inputUsdValue?.eq(0)) {
152
156
  resetState(false);
153
157
  cancelFetch();
@@ -175,6 +179,7 @@ export function useSwapInput(): UseSwapInput {
175
179
  });
176
180
  return cancelFetch;
177
181
  }, [
182
+ fetchStatus,
178
183
  inputAmount,
179
184
  fromToken?.symbol,
180
185
  fromToken?.address,
@@ -1,6 +1,6 @@
1
1
  import type { Asset } from 'rango-sdk';
2
2
 
3
- import { useEffect, useLayoutEffect, useRef } from 'react';
3
+ import { useEffect, useRef } from 'react';
4
4
 
5
5
  import { useAppStore } from '../store/AppStore';
6
6
  import { useQuoteStore } from '../store/quote';
@@ -44,14 +44,7 @@ export function useSyncStoresWithConfig() {
44
44
  setInputAmount(config?.amount?.toString() || '');
45
45
  }, [config?.amount]);
46
46
 
47
- /*
48
- * We update quote tokens in two scenarios.
49
- * If default values exist in widget config or search parameters exist in URL.
50
- * The logic for updating quote store exists in two useEffect in two locations, and they run in order.If default tokens for quote exist in widget config and also exist in URL, widget config values take precedence over URL values, and URL search parameters don’t affect the widget.
51
- * Using useLayoutEffect causes widget config values to apply first.
52
- * We may consider replacing this with a better solution in the future.
53
- */
54
- useLayoutEffect(() => {
47
+ useEffect(() => {
55
48
  if (fetchMetaStatus === 'success') {
56
49
  const chain = blockchains.find(
57
50
  (chain) => chain.name === config?.from?.blockchain
@@ -100,14 +93,7 @@ export function useSyncStoresWithConfig() {
100
93
  }
101
94
  }, [toTokensConfig, toBlockchainsConfig]);
102
95
 
103
- /*
104
- * We update quote tokens in two scenarios.
105
- * If default values exist in widget config or search parameters exist in URL.
106
- * The logic for updating quote store exists in two useEffect in two locations, and they run in order.If default tokens for quote exist in widget config and also exist in URL, widget config values take precedence over URL values, and URL search parameters don’t affect the widget.
107
- * Using useLayoutEffect causes widget config values to apply first.
108
- * We may consider replacing this with a better solution in the future.
109
- */
110
- useLayoutEffect(() => {
96
+ useEffect(() => {
111
97
  if (fetchMetaStatus === 'success') {
112
98
  const chain = blockchains.find(
113
99
  (chain) => chain.name === config?.to?.blockchain
@@ -0,0 +1 @@
1
+ export { useSyncUrlAndStore } from './useSyncUrlAndStore';
@@ -0,0 +1,35 @@
1
+ import type { BlockchainMeta, Token } from 'rango-sdk';
2
+
3
+ export function searchParamToToken(
4
+ tokens: Token[],
5
+ searchParam: string | null,
6
+ chain: BlockchainMeta | null
7
+ ): Token | null {
8
+ if (!chain) {
9
+ return null;
10
+ }
11
+ return (
12
+ tokens.find((token) => {
13
+ const symbolAndAddress = searchParam?.split('--');
14
+ if (symbolAndAddress?.length === 1) {
15
+ return (
16
+ token.symbol === symbolAndAddress[0] &&
17
+ token.address === null &&
18
+ token.blockchain === chain.name
19
+ );
20
+ }
21
+ return (
22
+ token.symbol === symbolAndAddress?.[0] &&
23
+ token.address === symbolAndAddress?.[1] &&
24
+ token.blockchain === chain.name
25
+ );
26
+ }) || null
27
+ );
28
+ }
29
+
30
+ export function tokenToSearchParam(token: Token | null): string | undefined {
31
+ if (token) {
32
+ return `${token.symbol}${token.address ? `--${token.address}` : ''}`;
33
+ }
34
+ return undefined;
35
+ }
@@ -0,0 +1,135 @@
1
+ import { useEffect } from 'react';
2
+ import {
3
+ useInRouterContext,
4
+ useLocation,
5
+ useSearchParams,
6
+ } from 'react-router-dom';
7
+
8
+ import { SearchParams } from '../../constants/searchParams';
9
+ import { useAppStore } from '../../store/AppStore';
10
+ import { useQuoteStore } from '../../store/quote';
11
+
12
+ import {
13
+ searchParamToToken,
14
+ tokenToSearchParam,
15
+ } from './useSyncUrlAndStore.helpers';
16
+
17
+ export function useSyncUrlAndStore() {
18
+ const location = useLocation();
19
+ const [searchParams, setSearchParams] = useSearchParams();
20
+ const {
21
+ fromBlockchain,
22
+ toBlockchain,
23
+ fromToken,
24
+ toToken,
25
+ inputAmount,
26
+ setFromBlockchain,
27
+ setToBlockchain,
28
+ setFromToken,
29
+ setToToken,
30
+ setInputAmount,
31
+ } = useQuoteStore();
32
+ const fetchMetaStatus = useAppStore().fetchStatus;
33
+ const blockchains = useAppStore().blockchains();
34
+ const tokens = useAppStore().tokens();
35
+ const isInRouterContext = useInRouterContext();
36
+
37
+ const getUrlSearchParams = () => {
38
+ const fromAmount = searchParams.get(SearchParams.FROM_AMOUNT);
39
+ const fromBlockchain = searchParams.get(SearchParams.FROM_BLOCKCHAIN);
40
+ const fromToken = searchParams.get(SearchParams.FROM_TOKEN);
41
+ const toBlockchain = searchParams.get(SearchParams.TO_BLOCKCHAIN);
42
+ const toToken = searchParams.get(SearchParams.TO_TOKEN);
43
+ const autoConnect = searchParams.get(SearchParams.AUTO_CONNECT);
44
+
45
+ return {
46
+ fromAmount,
47
+ fromBlockchain,
48
+ fromToken,
49
+ toBlockchain,
50
+ toToken,
51
+ autoConnect,
52
+ };
53
+ };
54
+
55
+ const updateUrlSearchParams = (
56
+ searchParams: Record<string, string | undefined>
57
+ ) => {
58
+ for (const property in searchParams) {
59
+ if (!searchParams[property]) {
60
+ delete searchParams[property];
61
+ }
62
+ }
63
+ setSearchParams(searchParams as Record<string, string>, { replace: true });
64
+ };
65
+
66
+ useEffect(() => {
67
+ const { autoConnect } = getUrlSearchParams();
68
+ if (isInRouterContext && fetchMetaStatus === 'success') {
69
+ updateUrlSearchParams({
70
+ [SearchParams.FROM_BLOCKCHAIN]: fromBlockchain?.name,
71
+ [SearchParams.FROM_TOKEN]: tokenToSearchParam(fromToken),
72
+ [SearchParams.TO_BLOCKCHAIN]: toBlockchain?.name,
73
+ [SearchParams.TO_TOKEN]: tokenToSearchParam(toToken),
74
+ [SearchParams.FROM_AMOUNT]: inputAmount,
75
+ [SearchParams.AUTO_CONNECT]: autoConnect ?? undefined,
76
+ });
77
+ }
78
+ }, [
79
+ location.pathname,
80
+ inputAmount,
81
+ fromBlockchain,
82
+ fromToken,
83
+ toBlockchain,
84
+ toToken,
85
+ ]);
86
+
87
+ useEffect(() => {
88
+ if (!isInRouterContext) {
89
+ return;
90
+ }
91
+ const searchParams = getUrlSearchParams();
92
+ if (searchParams.fromAmount) {
93
+ setInputAmount(searchParams.fromAmount);
94
+ }
95
+ if (fetchMetaStatus === 'success') {
96
+ const fromBlockchain = blockchains.find(
97
+ (blockchain) => blockchain.name === searchParams.fromBlockchain
98
+ );
99
+ const fromToken = searchParamToToken(
100
+ tokens,
101
+ searchParams.fromToken,
102
+ fromBlockchain ?? null
103
+ );
104
+ const toBlockchain = blockchains.find(
105
+ (blockchain) => blockchain.name === searchParams.toBlockchain
106
+ );
107
+ const toToken = searchParamToToken(
108
+ tokens,
109
+ searchParams.toToken,
110
+ toBlockchain ?? null
111
+ );
112
+ if (!!fromBlockchain) {
113
+ setFromBlockchain(fromBlockchain);
114
+ if (!!fromToken) {
115
+ setFromToken({
116
+ token: fromToken,
117
+ meta: {
118
+ blockchains: blockchains,
119
+ tokens: tokens,
120
+ },
121
+ });
122
+ }
123
+ }
124
+ if (!!toBlockchain) {
125
+ setToBlockchain(toBlockchain);
126
+ if (!!toToken) {
127
+ setToToken({
128
+ token: toToken,
129
+ meta: { blockchains: blockchains, tokens: tokens },
130
+ });
131
+ }
132
+ }
133
+ }
134
+ }, [fetchMetaStatus]);
135
+ }
@@ -0,0 +1,2 @@
1
+ export { TabManager } from './tabManager';
2
+ export type { TabManagerInterface } from './tabManager.types';
@@ -0,0 +1,3 @@
1
+ export const TIMEOUT = 300;
2
+ export const MAXIMUM_TAB_ID = 100_000;
3
+ export const CHANNEL_NAME = 'rango-widget';
@@ -0,0 +1,222 @@
1
+ import type {
2
+ Events,
3
+ ID,
4
+ Message,
5
+ TabManagerInterface,
6
+ } from './tabManager.types';
7
+
8
+ import { CHANNEL_NAME, MAXIMUM_TAB_ID, TIMEOUT } from './tabManager.constants';
9
+
10
+ export class TabManager implements TabManagerInterface {
11
+ private tabId: ID;
12
+ private state: 'not-initiated' | 'not-claimed' | 'claimed' = 'not-initiated';
13
+ private lastTryClaim: number | undefined;
14
+ private events: Events = {};
15
+ private channel: BroadcastChannel;
16
+
17
+ constructor(events: Events) {
18
+ this.channel = new BroadcastChannel(CHANNEL_NAME);
19
+ this.tabId = Math.trunc(Math.random() * MAXIMUM_TAB_ID);
20
+ this.events = events;
21
+ this.initEvents();
22
+ void this.tryClaim();
23
+ }
24
+
25
+ /**
26
+ * This is applicable when a user in an inactive tab seeks to activate that tab.
27
+ */
28
+ forceClaim = () => {
29
+ if (!this.isClaimed()) {
30
+ const message: Message = { name: 'force-claim', candidateId: this.tabId };
31
+ this.channel.postMessage(message);
32
+ setTimeout(() => {
33
+ if (!this.isClaimed()) {
34
+ this.claim();
35
+ }
36
+ }, TIMEOUT);
37
+ }
38
+ };
39
+
40
+ isClaimed = () => {
41
+ return this.state === 'claimed';
42
+ };
43
+
44
+ /**
45
+ * remove event listeners
46
+ */
47
+ destroy = () => {
48
+ this.channel.removeEventListener('message', this.handleMessageEvent);
49
+ document.removeEventListener(
50
+ 'visibilitychange',
51
+ this.handleVisibilityChange
52
+ );
53
+ document.removeEventListener('resume', this.handleResume);
54
+ };
55
+
56
+ /**
57
+ * Reacting to messages and window events to claim.
58
+ */
59
+ private initEvents() {
60
+ this.channel.addEventListener('message', this.handleMessageEvent);
61
+
62
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
63
+
64
+ /**
65
+ * The "resume" event is currently supported only in certain browsers.
66
+ * It is triggered when a previously frozen tab is unfrozen and resumes execution.
67
+ * To avoid having multiple active tabs,
68
+ * if the current tab is active after the "resume" event,
69
+ * we send a signal to other tabs to verify if any of them currently claim to be active.
70
+ * If this is the case, we deactivate the current tab.
71
+ * https://developer.chrome.com/docs/web-platform/page-lifecycle-api
72
+ */
73
+ document.addEventListener('resume', this.handleResume);
74
+ }
75
+
76
+ /**
77
+ * Runs after a `ping` message and checks if current tab is claimed the tabs,
78
+ * if yes, it responds with `pong`, If not, it doesn't do anything.
79
+ *
80
+ * Note 1: If we receive a `ping` message,
81
+ * it means a tab is trying to claim tabs directly, usually it's the active tab.
82
+ *
83
+ * Note 2: If multiple tabs are opening at once, all of them are broadcasting `ping`
84
+ * and eventually all of them will `claim` the queue after the TIMEOUT.
85
+ * For avoiding this, we will accepts the oldest broadcasted ping. (`isPingSentBefore`)
86
+ */
87
+ private claimedByCurrentTab(pingAt: number) {
88
+ const isPingSentBefore = this.lastTryClaim && this.lastTryClaim < pingAt;
89
+
90
+ if (this.isClaimed() || isPingSentBefore) {
91
+ const message: Message = {
92
+ name: 'pong',
93
+ };
94
+ this.channel.postMessage(message);
95
+ return;
96
+ }
97
+ }
98
+
99
+ private handleMessageEvent = (event: MessageEvent<Message>) => {
100
+ const name = event.data.name;
101
+
102
+ switch (name) {
103
+ case 'ping':
104
+ this.claimedByCurrentTab(event.data.pingAt);
105
+ break;
106
+
107
+ case 'pong':
108
+ this.alreadyClaimedByAnotherTab();
109
+ break;
110
+
111
+ case 'force-claim':
112
+ this.forceRelease(event.data.candidateId);
113
+ break;
114
+
115
+ case 'force-release':
116
+ if (this.tabId === event.data.candidateId) {
117
+ this.claim();
118
+ }
119
+ break;
120
+
121
+ default:
122
+ throw new Error(`${name} is not supported.`);
123
+ }
124
+ };
125
+
126
+ private handleVisibilityChange = async () => {
127
+ if (document.visibilityState === 'visible') {
128
+ await this.tryClaim();
129
+ }
130
+ };
131
+
132
+ private handleResume = async () => {
133
+ if (this.isClaimed()) {
134
+ await this.tryClaim();
135
+ if (!this.isClaimed()) {
136
+ this.events.onRelease?.();
137
+ }
138
+ }
139
+ };
140
+ /**
141
+ * Runs after receiving `pong` message, which means one of tabs already claimed the tabs.
142
+ */
143
+ private alreadyClaimedByAnotherTab() {
144
+ this.resetLastCheck();
145
+ }
146
+
147
+ /**
148
+ * Try to send a `ping` message to know if the tabs `claimed` or not.
149
+ * On constructing instance, or whenever page is active we will try.
150
+ */
151
+ private async tryClaim() {
152
+ /**
153
+ * Try to ask from possible other running tabs.
154
+ */
155
+ this.setLastCheck();
156
+ return new Promise<void>((resolve) => {
157
+ setTimeout(() => {
158
+ this.check();
159
+ resolve();
160
+ }, TIMEOUT);
161
+ });
162
+ }
163
+
164
+ /**
165
+ * If `lastCheck` is exist, it means there is no other tabs which `claimed` the tabs.
166
+ * And we mark the current tab as `claimed`
167
+ */
168
+ private check() {
169
+ if (this.state === 'not-initiated') {
170
+ this.events.onInit?.();
171
+ }
172
+ /**
173
+ *it means we didn't get any response from other tabs, this tab can claim.
174
+ */
175
+ if (this.lastTryClaim) {
176
+ this.claim();
177
+ this.resetLastCheck();
178
+ } else {
179
+ this.state = 'not-claimed';
180
+ }
181
+ }
182
+
183
+ private claim() {
184
+ this.state = 'claimed';
185
+ this.events.onClaim?.();
186
+ }
187
+
188
+ /**
189
+ * Setting current time as `lastCheck` and broadcast a `ping` message event.
190
+ */
191
+ private setLastCheck() {
192
+ this.lastTryClaim = Date.now();
193
+ const message: Message = {
194
+ name: 'ping',
195
+ pingAt: this.lastTryClaim,
196
+ };
197
+ this.channel.postMessage(message);
198
+ }
199
+
200
+ private resetLastCheck() {
201
+ this.lastTryClaim = undefined;
202
+ }
203
+
204
+ /**
205
+ * Upon receipt of a "force-claim" message,
206
+ * the presently active tab is expected to deactivate itself and initiate any attached callback associated with this event.
207
+ */
208
+ private forceRelease(nextActiveTab: ID) {
209
+ if (this.isClaimed()) {
210
+ this.state = 'not-claimed';
211
+
212
+ this.events.onRelease?.();
213
+
214
+ const message: Message = {
215
+ name: 'force-release',
216
+ candidateId: nextActiveTab,
217
+ };
218
+
219
+ this.channel.postMessage(message);
220
+ }
221
+ }
222
+ }