mirror of
https://github.com/zhigang1992/wallet.git
synced 2026-04-29 05:05:32 +08:00
1
.github/workflows/build-extension.yml
vendored
1
.github/workflows/build-extension.yml
vendored
@@ -3,6 +3,7 @@ on: [pull_request, workflow_dispatch]
|
||||
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }}
|
||||
|
||||
jobs:
|
||||
pre_run:
|
||||
|
||||
1
.github/workflows/publish-extensions.yml
vendored
1
.github/workflows/publish-extensions.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY }}
|
||||
|
||||
jobs:
|
||||
pre-run:
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"@rehooks/document-title": "1.0.2",
|
||||
"@sentry/react": "6.12.0",
|
||||
"@sentry/tracing": "6.12.0",
|
||||
"@segment/analytics-next": "1.31.1",
|
||||
"@stacks/blockchain-api-client": "0.65.0",
|
||||
"@stacks/common": "2.0.1",
|
||||
"@stacks/connect-ui": "5.2.0",
|
||||
@@ -240,6 +241,7 @@
|
||||
"bn.js": "5.2.0",
|
||||
"buffer": "6.0.3",
|
||||
"css-what": "5.0.1",
|
||||
"dot-prop": "6.0.1",
|
||||
"hosted-git-info": "4.0.2",
|
||||
"immer": "9.0.6",
|
||||
"mixme": "0.5.2",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { AppErrorBoundary } from '@features/errors/app-error-boundary';
|
||||
import { TransactionSettingsDrawer } from '@features/fee-nonce-drawers/transaction-settings-drawer';
|
||||
import { SpeedUpTransactionDrawer } from '@features/fee-nonce-drawers/speed-up-transaction-drawer';
|
||||
import { Devtools } from '@features/devtool/devtools';
|
||||
import { initSegment } from '@common/segment-init';
|
||||
|
||||
const devToolsEnabled = false;
|
||||
|
||||
@@ -27,6 +28,7 @@ declare global {
|
||||
}
|
||||
|
||||
window.__APP_VERSION__ = VERSION;
|
||||
void initSegment();
|
||||
|
||||
export const App: React.FC = () => {
|
||||
return (
|
||||
|
||||
113
src/common/hooks/analytics/transactions-analytics.hooks.ts
Normal file
113
src/common/hooks/analytics/transactions-analytics.hooks.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { statusFromTx } from '@common/api/transactions';
|
||||
import { isAddressTransactionWithTransfers } from '@common/transactions/transaction-utils';
|
||||
import { useCurrentAccount } from '@store/accounts/account.hooks';
|
||||
import {
|
||||
AddressTransactionWithTransfers,
|
||||
MempoolTransaction,
|
||||
} from '@stacks/stacks-blockchain-api-types';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAnalytics } from './use-analytics';
|
||||
import { StacksTransaction } from '@stacks/transactions';
|
||||
|
||||
let previousAccountTransactions: Map<string, TxStatus>;
|
||||
|
||||
function changedTransactions(previousTxs: Map<string, TxStatus>, txs: Map<string, TxStatus>) {
|
||||
if (!previousTxs) return;
|
||||
const result = new Map(
|
||||
[...txs]
|
||||
.filter(
|
||||
([key, value]) =>
|
||||
(!previousTxs.get(key) && value.status === 'submitted') ||
|
||||
(previousTxs.get(key) && previousTxs.get(key)?.status !== value.status)
|
||||
)
|
||||
.map(([key, value]) => {
|
||||
const timeSinceLastState =
|
||||
previousTxs.get(key) && dayjs(value.timeIso).diff(previousTxs.get(key)?.timeIso);
|
||||
return [
|
||||
key,
|
||||
{ ...value, previous_state: previousTxs.get(key)?.status, timeSinceLastState },
|
||||
];
|
||||
})
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
interface TxStatus {
|
||||
timeIso: string;
|
||||
broadcastTimeIso: string;
|
||||
status: string;
|
||||
type: 'inbound' | 'outbound';
|
||||
timeSinceBroadcast: number;
|
||||
tx: AddressTransactionWithTransfers | MempoolTransaction | StacksTransaction;
|
||||
}
|
||||
|
||||
type localTx = {
|
||||
[key: string]: {
|
||||
transaction: StacksTransaction;
|
||||
timestamp: number;
|
||||
};
|
||||
};
|
||||
|
||||
export function useTrackChangedTransactions(
|
||||
transactions: (AddressTransactionWithTransfers | MempoolTransaction)[],
|
||||
localTransactions: localTx
|
||||
) {
|
||||
const currentAccount = useCurrentAccount();
|
||||
const analytics = useAnalytics();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const result = new Map(previousAccountTransactions);
|
||||
Object.keys(localTransactions).forEach(key => {
|
||||
const tx = localTransactions[key];
|
||||
const status = 'submitted';
|
||||
result.set(key, {
|
||||
timeIso: now,
|
||||
broadcastTimeIso: now,
|
||||
timeSinceBroadcast: 0,
|
||||
status,
|
||||
type: 'outbound',
|
||||
tx: tx.transaction,
|
||||
});
|
||||
});
|
||||
|
||||
transactions.forEach(atx => {
|
||||
let time, tx;
|
||||
let type: 'inbound' | 'outbound';
|
||||
if (isAddressTransactionWithTransfers(atx)) {
|
||||
tx = atx.tx;
|
||||
type = tx.sender_address === currentAccount?.address ? 'outbound' : 'inbound';
|
||||
time = ('burn_block_time_iso' in tx && tx.burn_block_time_iso) || now;
|
||||
} else {
|
||||
tx = atx;
|
||||
type = 'outbound';
|
||||
time = ('receipt_time_iso' in tx && tx.receipt_time_iso) || now;
|
||||
}
|
||||
|
||||
const status = statusFromTx(tx);
|
||||
if (status === 'success_microblock') {
|
||||
time = now; // since there is no real timestamp for microblocks, we just use the current time
|
||||
}
|
||||
const broadcastTimeIso = `${
|
||||
previousAccountTransactions?.get(tx.tx_id)?.broadcastTimeIso || now
|
||||
}`;
|
||||
const timeSinceBroadcast = dayjs(time).diff(broadcastTimeIso);
|
||||
result.set(tx.tx_id, {
|
||||
type,
|
||||
timeIso: time.toString(),
|
||||
status,
|
||||
tx: atx,
|
||||
broadcastTimeIso,
|
||||
timeSinceBroadcast,
|
||||
});
|
||||
});
|
||||
|
||||
const changed = changedTransactions(previousAccountTransactions, result);
|
||||
|
||||
previousAccountTransactions = result;
|
||||
if (!changed) return result;
|
||||
|
||||
[...changed].forEach(([_, value]) =>
|
||||
analytics.track('change_transaction_state', { ...value, tx: undefined })
|
||||
);
|
||||
return result;
|
||||
}
|
||||
54
src/common/hooks/analytics/use-analytics.ts
Normal file
54
src/common/hooks/analytics/use-analytics.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useCurrentNetworkState } from '@store/network/networks.hooks';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { IS_TEST_ENV } from '@common/constants';
|
||||
import { EventParams, PageParams } from '@segment/analytics-next/dist/pkg/core/arguments-resolver';
|
||||
import { analytics } from '@common/segment-init';
|
||||
import { logger } from '@common/logger';
|
||||
|
||||
const IGNORED_PATH_REGEXPS = [/^\/$/];
|
||||
|
||||
function isIgnoredPath(path: string) {
|
||||
return IGNORED_PATH_REGEXPS.find(regexp => regexp.test(path));
|
||||
}
|
||||
|
||||
function isHiroApiUrl(url: string) {
|
||||
return /^https:\/\/.*\.stacks.co/.test(url);
|
||||
}
|
||||
|
||||
export function useAnalytics() {
|
||||
const currentNetwork = useCurrentNetworkState();
|
||||
const location = useLocation();
|
||||
const defaultProperties = {
|
||||
network: currentNetwork.name.toLowerCase(),
|
||||
usingDefaultHiroApi: isHiroApiUrl(currentNetwork.url),
|
||||
route: location.pathname,
|
||||
version: VERSION,
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
context: { ip: '0.0.0.0' },
|
||||
};
|
||||
|
||||
return {
|
||||
page: async (...args: PageParams) => {
|
||||
if (!analytics || IS_TEST_ENV) return;
|
||||
const [category, name, properties, options, ...rest] = args;
|
||||
|
||||
if (typeof name === 'string' && isIgnoredPath(name)) return;
|
||||
|
||||
const prop = { ...defaultProperties, ...properties };
|
||||
const opts = { ...defaultOptions, ...options };
|
||||
|
||||
return analytics.page(category, name, prop, opts, ...rest).catch(logger.error);
|
||||
},
|
||||
track: async (...args: EventParams) => {
|
||||
if (!analytics || IS_TEST_ENV) return;
|
||||
const [eventName, properties, options, ...rest] = args;
|
||||
|
||||
const prop = { ...defaultProperties, ...properties };
|
||||
const opts = { ...defaultOptions, ...options };
|
||||
|
||||
return analytics.track(eventName, prop, opts, ...rest).catch(logger.error);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
useSeedInputErrorState,
|
||||
useSeedInputState,
|
||||
} from '@store/onboarding/onboarding.hooks';
|
||||
import { useAnalytics } from '../analytics/use-analytics';
|
||||
|
||||
export function useSignIn() {
|
||||
const [, setMagicRecoveryCode] = useMagicRecoveryCodeState();
|
||||
@@ -22,6 +23,7 @@ export function useSignIn() {
|
||||
const { isLoading, setIsLoading, setIsIdle } = useLoading('useSignIn');
|
||||
const doChangeScreen = useChangeScreen();
|
||||
const { doStoreSeed } = useWallet();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
@@ -30,9 +32,10 @@ export function useSignIn() {
|
||||
setError(message);
|
||||
setIsIdle();
|
||||
textAreaRef.current?.focus();
|
||||
void analytics.track('submit_invalid_secret_key');
|
||||
return;
|
||||
},
|
||||
[setError, setIsIdle, textAreaRef]
|
||||
[analytics, setError, setIsIdle]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
@@ -61,6 +64,7 @@ export function useSignIn() {
|
||||
|
||||
try {
|
||||
await doStoreSeed({ secretKey: parsedKeyInput });
|
||||
void analytics.track('submit_valid_secret_key');
|
||||
doChangeScreen(ScreenPaths.SET_PASSWORD);
|
||||
setIsIdle();
|
||||
} catch (error) {
|
||||
@@ -68,13 +72,14 @@ export function useSignIn() {
|
||||
}
|
||||
},
|
||||
[
|
||||
seed,
|
||||
doStoreSeed,
|
||||
doChangeScreen,
|
||||
handleSetError,
|
||||
setIsIdle,
|
||||
setIsLoading,
|
||||
seed,
|
||||
handleSetError,
|
||||
setMagicRecoveryCode,
|
||||
doChangeScreen,
|
||||
doStoreSeed,
|
||||
analytics,
|
||||
setIsIdle,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { ScreenPaths } from '@common/types';
|
||||
import { IS_TEST_ENV } from '@common/constants';
|
||||
import { userHasAllowedDiagnosticsKey } from '@store/onboarding/onboarding.hooks';
|
||||
|
||||
import { useChangeScreen } from './use-change-screen';
|
||||
|
||||
export function usePromptUserToSetDiagnosticPermissions() {
|
||||
const changeScreen = useChangeScreen();
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_TEST_ENV) return;
|
||||
const persistedUserDiagnosticDecision = localStorage.getItem(userHasAllowedDiagnosticsKey);
|
||||
if (persistedUserDiagnosticDecision === null) changeScreen(ScreenPaths.REQUEST_DIAGNOSTICS);
|
||||
}, [changeScreen]);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { useTabState } from '@store/ui/ui.hooks';
|
||||
|
||||
export function useHomeTabs() {
|
||||
const [activeTab, setActiveTab] = useTabState('HOME_TABS');
|
||||
|
||||
const setActiveTabBalances = () => setActiveTab(0);
|
||||
const setActiveTabActivity = () => setActiveTab(1);
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getTicker, initBigNumber } from '@common/utils';
|
||||
import { ftDecimals, stacksValue } from '@common/stacks-utils';
|
||||
import { useCurrentAccountAvailableStxBalance } from '@store/accounts/account.hooks';
|
||||
import { useSelectedAssetState, useUpdateSelectedAsset } from '@store/assets/asset.hooks';
|
||||
import { useAnalytics } from './analytics/use-analytics';
|
||||
|
||||
export function getFullyQualifiedAssetName(asset?: AssetWithMeta) {
|
||||
return asset ? `${asset.contractAddress}.${asset.contractName}::${asset.name}` : undefined;
|
||||
@@ -15,11 +16,13 @@ export function useSelectedAsset() {
|
||||
const selectedAsset = useSelectedAssetState();
|
||||
const setSelectedAsset = useUpdateSelectedAsset();
|
||||
const availableStxBalance = useCurrentAccountAvailableStxBalance();
|
||||
const analytics = useAnalytics();
|
||||
const handleUpdateSelectedAsset = useCallback(
|
||||
(asset: AssetWithMeta | undefined) => {
|
||||
setSelectedAsset(getFullyQualifiedAssetName(asset) || undefined);
|
||||
void analytics.track('select_asset_for_send');
|
||||
},
|
||||
[setSelectedAsset]
|
||||
[analytics, setSelectedAsset]
|
||||
);
|
||||
const name = selectedAsset?.meta?.name || selectedAsset?.name;
|
||||
const isStx = selectedAsset?.name === 'Stacks Token';
|
||||
|
||||
@@ -3,9 +3,12 @@ import { useCallback } from 'react';
|
||||
import { SetPassword, StoreSeed, UnlockWallet, SwitchAccount } from '@background/vault-types';
|
||||
import { InternalMethods } from '@common/message-types';
|
||||
import { useInnerMessageWrapper } from '@store/wallet/wallet.hooks';
|
||||
import { clearSessionLocalData } from '@common/store-utils';
|
||||
import { useAnalytics } from './analytics/use-analytics';
|
||||
|
||||
export function useVaultMessenger() {
|
||||
const innerMessageWrapper = useInnerMessageWrapper();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const doSetPassword = useCallback(
|
||||
(payload: string) => {
|
||||
@@ -64,7 +67,8 @@ export function useVaultMessenger() {
|
||||
innerMessageWrapper({ method: InternalMethods.signOut, payload: undefined });
|
||||
const doSignOut = async () => {
|
||||
await handleSignOut();
|
||||
localStorage.clear();
|
||||
void analytics.track('sign_out');
|
||||
clearSessionLocalData();
|
||||
};
|
||||
const doLockWallet = () =>
|
||||
innerMessageWrapper({ method: InternalMethods.lockWallet, payload: undefined });
|
||||
|
||||
36
src/common/segment-init.ts
Normal file
36
src/common/segment-init.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Analytics, AnalyticsBrowser } from '@segment/analytics-next';
|
||||
import { IS_TEST_ENV } from './constants';
|
||||
import { logger } from './logger';
|
||||
import { checkUserHasGrantedPermission } from './sentry-init';
|
||||
|
||||
export let analytics: Analytics;
|
||||
|
||||
export function initSegment() {
|
||||
const writeKey = process.env.SEGMENT_WRITE_KEY;
|
||||
const hasPermission = checkUserHasGrantedPermission();
|
||||
if (!hasPermission) {
|
||||
logger.info('segment init aborted: has no permission.');
|
||||
return;
|
||||
}
|
||||
if (IS_TEST_ENV) return;
|
||||
if (!writeKey) {
|
||||
logger.info('segment init aborted: No WRITE_KEY setup.');
|
||||
return;
|
||||
}
|
||||
|
||||
return AnalyticsBrowser.standalone(writeKey, {
|
||||
integrations: {
|
||||
'Segment.io': {
|
||||
deliveryStrategy: {
|
||||
strategy: 'batching',
|
||||
config: {
|
||||
size: 10,
|
||||
timeout: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(res => (analytics = res))
|
||||
.catch(logger.error);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Integrations } from '@sentry/tracing';
|
||||
|
||||
import { userHasAllowedDiagnosticsKey } from '@store/onboarding/onboarding.hooks';
|
||||
|
||||
function checkUserHasGrantedPermission() {
|
||||
export function checkUserHasGrantedPermission() {
|
||||
return localStorage.getItem(userHasAllowedDiagnosticsKey) === 'true';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { userHasAllowedDiagnosticsKey } from '@store/onboarding/onboarding.hooks';
|
||||
import hash from 'object-hash';
|
||||
import { hashQueryKey, QueryKey } from 'react-query';
|
||||
|
||||
@@ -25,3 +26,19 @@ export function setLocalData<Data>(params: string[], data: Data): Data {
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
return data;
|
||||
}
|
||||
|
||||
// LocalStorage keys kept across sign-in/signout sessions
|
||||
const PERSISTENT_LOCAL_DATA: string[] = [userHasAllowedDiagnosticsKey];
|
||||
|
||||
export function clearSessionLocalData() {
|
||||
const backup = PERSISTENT_LOCAL_DATA.map((key: string) => [key, localStorage.getItem(key)]);
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
// Store the backup in localStorage
|
||||
backup.forEach(([key, value]) => {
|
||||
if (key === null || value === null) return;
|
||||
localStorage.setItem(key, value);
|
||||
});
|
||||
return localStorage;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Flex, Button, Stack } from '@stacks/ui';
|
||||
import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { Body } from '@components/typography';
|
||||
import { SettingsSelectors } from '@tests/integration/settings.selectors';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
interface CreateAccountProps {
|
||||
close: () => void;
|
||||
@@ -14,15 +15,17 @@ export const CreateAccount: React.FC<CreateAccountProps> = ({ close }) => {
|
||||
const { doCreateNewAccount } = useWallet();
|
||||
const [isSetting, setSetting] = useState(false);
|
||||
const [hasFired, setHasFired] = useState(false);
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const createAccount = useCallback(async () => {
|
||||
if (!isSetting) {
|
||||
setSetting(true);
|
||||
await doCreateNewAccount();
|
||||
void analytics.track('create_new_account');
|
||||
setSetting(false);
|
||||
window.setTimeout(() => close(), TIMEOUT);
|
||||
}
|
||||
}, [doCreateNewAccount, isSetting, setSetting, close]);
|
||||
}, [isSetting, doCreateNewAccount, analytics, close]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasFired) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
StxTransfer,
|
||||
} from '@common/transactions/transaction-utils';
|
||||
import { useAssetByIdentifier } from '@store/assets/asset.hooks';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
type Tx = MempoolTransaction | Transaction;
|
||||
|
||||
@@ -260,6 +261,11 @@ export const TxItem = ({ transaction, ...rest }: TxItemProps) => {
|
||||
const [component, bind, { isHovered }] = usePressable(true);
|
||||
const { handleOpenTxLink } = useExplorerLink();
|
||||
const currentAccount = useCurrentAccount();
|
||||
const analytics = useAnalytics();
|
||||
const openTxLink = () => {
|
||||
void analytics.track('view_transaction');
|
||||
handleOpenTxLink(transaction.tx_id);
|
||||
};
|
||||
|
||||
if (!transaction) return null;
|
||||
|
||||
@@ -277,7 +283,7 @@ export const TxItem = ({ transaction, ...rest }: TxItemProps) => {
|
||||
isInline
|
||||
position="relative"
|
||||
zIndex={2}
|
||||
onClick={() => handleOpenTxLink(transaction.tx_id)}
|
||||
onClick={openTxLink}
|
||||
>
|
||||
<TxItemIcon transaction={transaction} />
|
||||
<SpaceBetween flexGrow={1}>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useAccounts } from '@store/accounts/account.hooks';
|
||||
import { useUpdateAccountDrawerStep } from '@store/ui/ui.hooks';
|
||||
import { AccountStep } from '@store/ui/ui.models';
|
||||
import { AccountWithAddress } from '@store/accounts/account.models';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
interface SwitchAccountProps {
|
||||
close: () => void;
|
||||
@@ -148,11 +149,17 @@ const AccountList: React.FC<{ handleClose: () => void }> = memo(({ handleClose }
|
||||
|
||||
export const SwitchAccounts: React.FC<SwitchAccountProps> = memo(({ close }) => {
|
||||
const setAccountDrawerStep = useUpdateAccountDrawerStep();
|
||||
const analytics = useAnalytics();
|
||||
const setCreateAccountStep = () => {
|
||||
void analytics.track('choose_to_create_account');
|
||||
setAccountDrawerStep(AccountStep.Create);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountList handleClose={close} />
|
||||
<Box pt="base" pb="loose" px="loose">
|
||||
<Button onClick={() => setAccountDrawerStep(AccountStep.Create)}>Create an account</Button>
|
||||
<Button onClick={setCreateAccountStep}>Create an account</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useCurrentAccountLocalTxids } from '@store/accounts/account-activity.hooks';
|
||||
import {
|
||||
useCurrentAccountLocallySubmittedStacksTransactions,
|
||||
useCurrentAccountLocalTxids,
|
||||
} from '@store/accounts/account-activity.hooks';
|
||||
import { TransactionList } from '@components/popup/transaction-list';
|
||||
import { LocalTxList } from '@features/local-transaction-activity/local-tx-list';
|
||||
|
||||
import { NoAccountActivity } from './components/no-account-activity';
|
||||
import { useAccountTransactionsWithTransfers } from '@common/hooks/account/use-account-transactions-with-transfers.hooks';
|
||||
import { useCurrentAccountFilteredMempoolTransactionsState } from '@query/mempool/mempool.hooks';
|
||||
import { useTrackChangedTransactions } from '@common/hooks/analytics/transactions-analytics.hooks';
|
||||
|
||||
export const ActivityList = () => {
|
||||
const transactions = useAccountTransactionsWithTransfers();
|
||||
const pendingTransactions = useCurrentAccountFilteredMempoolTransactionsState();
|
||||
|
||||
const txids = useCurrentAccountLocalTxids();
|
||||
const localTxs = useCurrentAccountLocallySubmittedStacksTransactions();
|
||||
const allTransactions = [...pendingTransactions, ...transactions];
|
||||
useTrackChangedTransactions(allTransactions, localTxs);
|
||||
const hasTxs = txids.length > 0 || transactions.length > 0;
|
||||
|
||||
if (!hasTxs) return <NoAccountActivity />;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Box, BoxProps, color, Flex, Stack } from '@stacks/ui';
|
||||
import { useUpdateCurrentNetworkKey } from '@store/network/networks.hooks';
|
||||
import { NetworkStatusIndicator } from './components/network-status-indicator';
|
||||
import { useNetworkStatus } from 'query/network/network.hooks';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
export const NetworkListItem: React.FC<{ item: string } & BoxProps> = ({ item, ...props }) => {
|
||||
const { setShowNetworks } = useDrawers();
|
||||
@@ -16,11 +17,13 @@ export const NetworkListItem: React.FC<{ item: string } & BoxProps> = ({ item, .
|
||||
const network = networks[item];
|
||||
const isActive = item === currentNetworkKey;
|
||||
const isOnline = useNetworkStatus(network.url);
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const handleItemClick = useCallback(() => {
|
||||
void analytics.track('change_network');
|
||||
setCurrentNetworkKey(item);
|
||||
setTimeout(() => setShowNetworks(false), 25);
|
||||
}, [setCurrentNetworkKey, item, setShowNetworks]);
|
||||
}, [analytics, setCurrentNetworkKey, item, setShowNetworks]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -6,16 +6,19 @@ import { ScreenPaths } from '@common/types';
|
||||
import { useDrawers } from '@common/hooks/use-drawers';
|
||||
import { useShowNetworksStore } from '@store/ui/ui.hooks';
|
||||
import { NetworkList } from '@features/network-drawer/network-list';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
export const NetworksDrawer: React.FC = () => {
|
||||
const { setShowNetworks } = useDrawers();
|
||||
const [isShowing] = useShowNetworksStore();
|
||||
const doChangeScreen = useChangeScreen();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const handleAddNetworkClick = useCallback(() => {
|
||||
void analytics.track('add_network');
|
||||
setShowNetworks(false);
|
||||
doChangeScreen(ScreenPaths.ADD_NETWORK);
|
||||
}, [setShowNetworks, doChangeScreen]);
|
||||
}, [analytics, setShowNetworks, doChangeScreen]);
|
||||
|
||||
return (
|
||||
<ControlledDrawer
|
||||
|
||||
@@ -11,6 +11,7 @@ import { USERNAMES_ENABLED } from '@common/constants';
|
||||
import { forwardRefWithAs } from '@stacks/ui-core';
|
||||
import { SettingsSelectors } from '@tests/integration/settings.selectors';
|
||||
import { AccountStep } from '@store/ui/ui.models';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
const MenuWrapper = forwardRefWithAs((props, ref) => (
|
||||
<Box
|
||||
@@ -71,6 +72,7 @@ export const SettingsPopover: React.FC = () => {
|
||||
setShowSignOut,
|
||||
} = useDrawers();
|
||||
const changeScreen = useChangeScreen();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setShowSettings(false);
|
||||
@@ -141,6 +143,7 @@ export const SettingsPopover: React.FC = () => {
|
||||
<MenuItem
|
||||
data-testid={SettingsSelectors.ChangeNetworkAction}
|
||||
onClick={wrappedCloseCallback(() => {
|
||||
void analytics.track('choose_to_change_network');
|
||||
setShowNetworks(true);
|
||||
})}
|
||||
>
|
||||
@@ -155,6 +158,7 @@ export const SettingsPopover: React.FC = () => {
|
||||
{isSignedIn && (
|
||||
<MenuItem
|
||||
onClick={wrappedCloseCallback(() => {
|
||||
void analytics.track('lock_session');
|
||||
void doLockWallet();
|
||||
changeScreen(ScreenPaths.POPUP_HOME);
|
||||
})}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { Body } from '@components/typography';
|
||||
import { Body, Title } from '@components/typography';
|
||||
import { Box, Button, Flex, color, Stack } from '@stacks/ui';
|
||||
import { BaseDrawer } from '@components/drawer';
|
||||
import { FiCheck } from 'react-icons/fi';
|
||||
import { InitialPageSelectors } from '@tests/integration/initial-page.selectors';
|
||||
|
||||
interface ReasonToAllowDiagnosticsProps {
|
||||
text: string;
|
||||
@@ -23,43 +23,48 @@ interface AllowDiagnosticsLayoutProps {
|
||||
onUserAllowDiagnostics(): void;
|
||||
onUserDenyDiagnosticsPermissions(): void;
|
||||
}
|
||||
export const AllowDiagnosticsLayout: FC<AllowDiagnosticsLayoutProps> = props => {
|
||||
|
||||
export function AllowDiagnosticsFullLayout(props: AllowDiagnosticsLayoutProps) {
|
||||
const { onUserAllowDiagnostics, onUserDenyDiagnosticsPermissions } = props;
|
||||
|
||||
const title = 'Help us improve';
|
||||
|
||||
return (
|
||||
<BaseDrawer title={title} isShowing onClose={() => onUserDenyDiagnosticsPermissions()}>
|
||||
<Stack spacing="extra-loose" flexGrow={1} justifyContent="center">
|
||||
<Title as="h1" fontWeight={500} textAlign="center">
|
||||
Help us improve
|
||||
</Title>
|
||||
<Box mx="loose" mb="extra-loose">
|
||||
<Body>
|
||||
Hiro would like to gather anonymous data to help improve the experience of using Stacks
|
||||
apps and the wallet.
|
||||
We would like to gather de-identified usage data to help improve your experience with Hiro
|
||||
Wallet.
|
||||
</Body>
|
||||
<Stack mt="loose" spacing="base-tight">
|
||||
<ReasonToAllowDiagnostics text="Send anonymous data about page views and clicks" />
|
||||
<ReasonToAllowDiagnostics text="We'll never collect personal data such as your Stacks address, keys, balances, or IP address" />
|
||||
<ReasonToAllowDiagnostics text="We'll never share or sell any user data" />
|
||||
<ReasonToAllowDiagnostics text="This data is tied to randomly-generated IDs and not personal data, such as your Stacks addresses, keys, balance, or IP addresses" />
|
||||
<ReasonToAllowDiagnostics text="This data is used to generate and send crash reports, help us fix errors, and analyze trends and statistics" />
|
||||
</Stack>
|
||||
<Flex mt="loose" fontSize="14px">
|
||||
<Flex mt="loose" fontSize="14px" justifyContent="center">
|
||||
<Button
|
||||
type="button"
|
||||
size="lg"
|
||||
mode="primary"
|
||||
data-testid={InitialPageSelectors.AnalyticsAllow}
|
||||
onClick={() => onUserAllowDiagnostics()}
|
||||
mr="base-tight"
|
||||
>
|
||||
Yes, I'll help
|
||||
Allow
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
mode="tertiary"
|
||||
size="lg"
|
||||
ml="base"
|
||||
variant="link"
|
||||
onClick={() => onUserDenyDiagnosticsPermissions()}
|
||||
>
|
||||
No thanks
|
||||
Deny
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</BaseDrawer>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,26 +4,35 @@ import { ScreenPaths } from '@common/types';
|
||||
import { useChangeScreen } from '@common/hooks/use-change-screen';
|
||||
import { useHasAllowedDiagnostics } from '@store/onboarding/onboarding.hooks';
|
||||
|
||||
import { AllowDiagnosticsLayout } from './allow-diagnostics-layout';
|
||||
import { initSentry } from '@common/sentry-init';
|
||||
import { initSegment } from '@common/segment-init';
|
||||
import { AllowDiagnosticsFullLayout } from './allow-diagnostics-layout';
|
||||
import { PopupContainer } from '@components/popup/container';
|
||||
import { Header } from '@components/header';
|
||||
|
||||
export const AllowDiagnosticsDrawer = () => {
|
||||
export const AllowDiagnosticsFullPage = () => {
|
||||
const changeScreen = useChangeScreen();
|
||||
const [, setHasAllowedDiagnostics] = useHasAllowedDiagnostics();
|
||||
|
||||
const goHomeAndSetDiagnosticsPermissionTo = useCallback(
|
||||
(areDiagnosticsAllowed: boolean) => {
|
||||
changeScreen(ScreenPaths.HOME);
|
||||
(areDiagnosticsAllowed: boolean | undefined) => {
|
||||
if (typeof areDiagnosticsAllowed === undefined) return;
|
||||
setHasAllowedDiagnostics(areDiagnosticsAllowed);
|
||||
if (areDiagnosticsAllowed) initSentry();
|
||||
if (areDiagnosticsAllowed) {
|
||||
initSentry();
|
||||
void initSegment();
|
||||
}
|
||||
changeScreen(ScreenPaths.HOME);
|
||||
},
|
||||
[changeScreen, setHasAllowedDiagnostics]
|
||||
);
|
||||
|
||||
return (
|
||||
<AllowDiagnosticsLayout
|
||||
onUserDenyDiagnosticsPermissions={() => goHomeAndSetDiagnosticsPermissionTo(false)}
|
||||
onUserAllowDiagnostics={() => goHomeAndSetDiagnosticsPermissionTo(true)}
|
||||
/>
|
||||
<PopupContainer header={<Header hideActions />} requestType="auth">
|
||||
<AllowDiagnosticsFullLayout
|
||||
onUserDenyDiagnosticsPermissions={() => goHomeAndSetDiagnosticsPermissionTo(false)}
|
||||
onUserAllowDiagnostics={() => goHomeAndSetDiagnosticsPermissionTo(true)}
|
||||
/>
|
||||
</PopupContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,16 +5,26 @@ import type { StackProps } from '@stacks/ui';
|
||||
import { Tabs } from '@components/tabs';
|
||||
import { LoadingSpinner } from '@components/loading-spinner';
|
||||
import { useHomeTabs } from '@common/hooks/use-home-tabs';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
interface HomeTabsProps extends StackProps {
|
||||
balances: JSX.Element;
|
||||
activity: JSX.Element;
|
||||
}
|
||||
|
||||
const ANALYTICS_PATH = ['/balances', '/activity'];
|
||||
|
||||
export function HomeTabs(props: HomeTabsProps) {
|
||||
const { balances, activity, ...rest } = props;
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const { activeTab, setActiveTab } = useHomeTabs();
|
||||
|
||||
const setActiveTabTracked = (index: number) => {
|
||||
void analytics.page('view', ANALYTICS_PATH[index]);
|
||||
setActiveTab(index);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack flexGrow={1} spacing="extra-loose" {...rest}>
|
||||
<Tabs
|
||||
@@ -23,7 +33,7 @@ export function HomeTabs(props: HomeTabsProps) {
|
||||
{ slug: 'activity', label: 'Activity' },
|
||||
]}
|
||||
activeTab={activeTab}
|
||||
onTabClick={setActiveTab}
|
||||
onTabClick={setActiveTabTracked}
|
||||
/>
|
||||
<Flex position="relative" flexGrow={1}>
|
||||
{activeTab === 0 && (
|
||||
|
||||
@@ -8,10 +8,17 @@ import { FiCopy } from 'react-icons/fi';
|
||||
import { CurrentUserAvatar } from '@features/current-user/current-user-avatar';
|
||||
import { CurrentUsername } from '@features/current-user/current-user-name';
|
||||
import { UserAreaSelectors } from '@tests/integration/user-area.selectors';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
const UserAddress = memo((props: StackProps) => {
|
||||
const currentAccount = useCurrentAccount();
|
||||
const { onCopy, hasCopied } = useClipboard(currentAccount?.address || '');
|
||||
const analytics = useAnalytics();
|
||||
const copyToClipboard = () => {
|
||||
void analytics.track('copy_address_to_clipboard');
|
||||
onCopy();
|
||||
};
|
||||
|
||||
return currentAccount ? (
|
||||
<Stack isInline {...props}>
|
||||
<Caption>{truncateMiddle(currentAccount.address, 4)}</Caption>
|
||||
@@ -19,7 +26,7 @@ const UserAddress = memo((props: StackProps) => {
|
||||
<Stack>
|
||||
<Box
|
||||
_hover={{ cursor: 'pointer' }}
|
||||
onClick={onCopy}
|
||||
onClick={copyToClipboard}
|
||||
size="12px"
|
||||
color={color('text-caption')}
|
||||
data-testid={UserAreaSelectors.AccountCopyAddress}
|
||||
|
||||
@@ -10,13 +10,9 @@ import { Header } from '@components/header';
|
||||
import { HiroMessages } from '@features/hiro-messages/hiro-messages';
|
||||
import { ActivityList } from '@features/activity-list/account-activity';
|
||||
import { BalancesList } from '@features/balances-list/balances-list';
|
||||
import { usePromptUserToSetDiagnosticPermissions } from '@common/hooks/use-diagnostic-permission-prompt';
|
||||
|
||||
import { HomeTabs } from './components/home-tabs';
|
||||
|
||||
export const Home = () => {
|
||||
usePromptUserToSetDiagnosticPermissions();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PopupContainer
|
||||
|
||||
@@ -9,20 +9,23 @@ import { useOnboardingState } from '@common/hooks/auth/use-onboarding-state';
|
||||
import { Title, Body } from '@components/typography';
|
||||
import { Header } from '@components/header';
|
||||
import { InitialPageSelectors } from '@tests/integration/initial-page.selectors';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
const Actions: React.FC<StackProps> = props => {
|
||||
const { doMakeWallet } = useWallet();
|
||||
const { decodedAuthRequest } = useOnboardingState();
|
||||
const doChangeScreen = useChangeScreen();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const [isCreatingWallet, setIsCreatingWallet] = useState(false);
|
||||
const register = useCallback(async () => {
|
||||
setIsCreatingWallet(true);
|
||||
await doMakeWallet();
|
||||
void analytics.track('generate_new_secret_key');
|
||||
if (decodedAuthRequest) {
|
||||
doChangeScreen(ScreenPaths.SET_PASSWORD);
|
||||
}
|
||||
}, [doMakeWallet, doChangeScreen, decodedAuthRequest]);
|
||||
}, [doMakeWallet, analytics, decodedAuthRequest, doChangeScreen]);
|
||||
|
||||
return (
|
||||
<Stack justifyContent="center" spacing="loose" textAlign="center" {...props}>
|
||||
|
||||
@@ -11,12 +11,19 @@ import { truncateMiddle } from '@stacks/ui-utils';
|
||||
import { Tooltip } from '@components/tooltip';
|
||||
import { QrCode } from './components/address-qr-code';
|
||||
import { ReceiveTokensHeader } from './components/recieve-tokens-header';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
export const PopupReceive: React.FC = () => {
|
||||
const { currentAccount, currentAccountStxAddress } = useWallet();
|
||||
const doChangeScreen = useChangeScreen();
|
||||
const address = currentAccountStxAddress || '';
|
||||
const analytics = useAnalytics();
|
||||
const { onCopy, hasCopied } = useClipboard(address);
|
||||
const copyToClipboard = () => {
|
||||
void analytics.track('copy_address_to_clipboard');
|
||||
onCopy();
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupContainer
|
||||
header={<ReceiveTokensHeader onClose={() => doChangeScreen(ScreenPaths.POPUP_HOME)} />}
|
||||
@@ -38,7 +45,7 @@ export const PopupReceive: React.FC = () => {
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box mt="auto">
|
||||
<Button width="100%" onClick={onCopy} borderRadius="10px">
|
||||
<Button width="100%" onClick={copyToClipboard} borderRadius="10px">
|
||||
Copy your address
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import { useWallet } from '@common/hooks/use-wallet';
|
||||
import { Button, color, Stack, BoxProps, useClipboard, StackProps } from '@stacks/ui';
|
||||
import { Button, color, Stack, BoxProps, StackProps, useClipboard } from '@stacks/ui';
|
||||
import { PopupContainer } from '@components/popup/container';
|
||||
import { Body, Text } from '@components/typography';
|
||||
import { Card } from '@components/card';
|
||||
import { Header } from '@components/header';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
const SecretKeyMessage: React.FC<BoxProps> = props => {
|
||||
const { secretKey } = useWallet();
|
||||
@@ -37,6 +38,13 @@ const SecretKeyActions: React.FC<{ handleNext?: () => void } & StackProps> = ({
|
||||
}) => {
|
||||
const { secretKey } = useWallet();
|
||||
const { onCopy, hasCopied } = useClipboard(secretKey || '');
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const copyToClipboard = () => {
|
||||
void analytics.track('copy_secret_key_to_clipboard');
|
||||
onCopy();
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing="base" {...rest}>
|
||||
<Button
|
||||
@@ -47,7 +55,7 @@ const SecretKeyActions: React.FC<{ handleNext?: () => void } & StackProps> = ({
|
||||
color={color(hasCopied ? 'text-caption' : 'brand')}
|
||||
mode="tertiary"
|
||||
borderRadius="10px"
|
||||
onClick={hasCopied ? undefined : onCopy}
|
||||
onClick={hasCopied ? undefined : copyToClipboard}
|
||||
>
|
||||
{hasCopied ? 'Copied!' : 'Copy to clipboard'}
|
||||
</Button>
|
||||
@@ -70,16 +78,26 @@ export const SaveYourKeyView: React.FC<{
|
||||
onClose?: () => void;
|
||||
title?: string;
|
||||
hideActions?: boolean;
|
||||
}> = memo(({ title, handleNext, hideActions, onClose }) => (
|
||||
<PopupContainer
|
||||
header={
|
||||
<Header onClose={onClose} hideActions={hideActions} title={title || 'Save your Secret Key'} />
|
||||
}
|
||||
>
|
||||
<Stack spacing="loose">
|
||||
<SecretKeyMessage />
|
||||
<SecretKeyCard />
|
||||
<SecretKeyActions handleNext={handleNext || onClose} />
|
||||
</Stack>
|
||||
</PopupContainer>
|
||||
));
|
||||
}> = memo(({ title, handleNext, hideActions, onClose }) => {
|
||||
const analytics = useAnalytics();
|
||||
useEffect(() => {
|
||||
void analytics.page('view', '/save-your-secret-key');
|
||||
}, [analytics]);
|
||||
return (
|
||||
<PopupContainer
|
||||
header={
|
||||
<Header
|
||||
onClose={onClose}
|
||||
hideActions={hideActions}
|
||||
title={title || 'Save your Secret Key'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Stack spacing="loose">
|
||||
<SecretKeyMessage />
|
||||
<SecretKeyCard />
|
||||
<SecretKeyActions handleNext={handleNext || onClose} />
|
||||
</Stack>
|
||||
</PopupContainer>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useSendAmountFieldActions } from '../hooks/use-send-form';
|
||||
import { SendMaxWithSuspense } from './send-max-button';
|
||||
import { SendFormSelectors } from '@tests/page-objects/send-form.selectors';
|
||||
import { useCurrentAccountBalancesUnanchoredState } from '@store/accounts/account.hooks';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
interface AmountFieldProps extends StackProps {
|
||||
value: number;
|
||||
@@ -19,6 +20,7 @@ interface AmountFieldProps extends StackProps {
|
||||
export const AmountField = memo((props: AmountFieldProps) => {
|
||||
const { value, error, ...rest } = props;
|
||||
|
||||
const analytics = useAnalytics();
|
||||
const assets = useAssets();
|
||||
const balances = useCurrentAccountBalancesUnanchoredState();
|
||||
const { selectedAsset, placeholder } = useSelectedAsset();
|
||||
@@ -27,6 +29,11 @@ export const AmountField = memo((props: AmountFieldProps) => {
|
||||
setFieldValue,
|
||||
});
|
||||
|
||||
const handleSetSendMaxTracked = (feeRate: number) => {
|
||||
void analytics.track('select_maximum_amount_for_send');
|
||||
return handleSetSendMax(feeRate);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack {...rest}>
|
||||
<InputGroup flexDirection="column">
|
||||
@@ -52,7 +59,7 @@ export const AmountField = memo((props: AmountFieldProps) => {
|
||||
{balances && selectedAsset ? (
|
||||
<SendMaxWithSuspense
|
||||
showButton={Boolean(balances && selectedAsset)}
|
||||
onSetMax={feeRate => handleSetSendMax(feeRate)}
|
||||
onSetMax={feeRate => handleSetSendMaxTracked(feeRate)}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { debounce } from 'ts-debounce';
|
||||
import { Box, Button, Input, Stack } from '@stacks/ui';
|
||||
import { Form, Formik } from 'formik';
|
||||
@@ -14,6 +14,7 @@ import { USERNAMES_ENABLED } from '@common/constants';
|
||||
import { validatePassword, blankPasswordValidation } from '@common/validation/validate-password';
|
||||
import { Body, Caption } from '@components/typography';
|
||||
import { Header } from '@components/header';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
const HUMAN_REACTION_DEBOUNCE_TIME = 250;
|
||||
|
||||
@@ -33,6 +34,10 @@ export const SetPasswordPage: React.FC<SetPasswordProps> = ({
|
||||
const { doSetPassword, wallet, doFinishSignIn } = useWallet();
|
||||
const doChangeScreen = useChangeScreen();
|
||||
const { decodedAuthRequest } = useOnboardingState();
|
||||
const analytics = useAnalytics();
|
||||
useEffect(() => {
|
||||
void analytics.page('view', '/set-password');
|
||||
}, [analytics]);
|
||||
|
||||
const submit = useCallback(
|
||||
async (password: string) => {
|
||||
@@ -69,12 +74,13 @@ export const SetPasswordPage: React.FC<SetPasswordProps> = ({
|
||||
if (!password) return;
|
||||
setLoading(true);
|
||||
if (strengthResult.meetsAllStrengthRequirements) {
|
||||
void analytics.track('submit_valid_password');
|
||||
await submit(password);
|
||||
return;
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[strengthResult, submit]
|
||||
[strengthResult, submit, analytics]
|
||||
);
|
||||
|
||||
const validationSchema = yup.object({
|
||||
@@ -87,6 +93,9 @@ export const SetPasswordPage: React.FC<SetPasswordProps> = ({
|
||||
if (typeof value !== 'string') return false;
|
||||
const result = validatePassword(value);
|
||||
setStrengthResult(result);
|
||||
if (!result.meetsAllStrengthRequirements) {
|
||||
void analytics.track('submit_invalid_password');
|
||||
}
|
||||
return result.meetsAllStrengthRequirements;
|
||||
}, HUMAN_REACTION_DEBOUNCE_TIME),
|
||||
}),
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useSetLocalTxsCallback } from '@store/accounts/account-activity.hooks';
|
||||
import { todaysIsoDate } from '@common/date-utils';
|
||||
import { logger } from '@common/logger';
|
||||
import { useCurrentAccountTxIds } from '@query/transactions/transaction.hooks';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
|
||||
function getErrorMessage(
|
||||
reason: TxBroadcastResultRejected['reason'] | 'ConflictingNonceInMempool'
|
||||
@@ -52,6 +53,7 @@ export function useSubmitTransactionCallback({
|
||||
const { setActiveTabActivity } = useHomeTabs();
|
||||
const setLocalTxs = useSetLocalTxsCallback();
|
||||
const externalTxid = useCurrentAccountTxIds();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
return useCallback<(tx: StacksTransaction) => Promise<void>>(
|
||||
async transaction => {
|
||||
@@ -74,6 +76,7 @@ export function useSubmitTransactionCallback({
|
||||
}
|
||||
if (nonce) await doSetLatestNonce(nonce);
|
||||
toast.success('Transaction submitted!');
|
||||
void analytics.track('broadcast_transaction');
|
||||
doChangeScreen(ScreenPaths.HOME);
|
||||
onClose();
|
||||
setIsIdle();
|
||||
@@ -89,17 +92,18 @@ export function useSubmitTransactionCallback({
|
||||
}
|
||||
},
|
||||
[
|
||||
setLocalTxs,
|
||||
doSetLatestNonce,
|
||||
replaceByFee,
|
||||
onClose,
|
||||
refreshAccountData,
|
||||
doChangeScreen,
|
||||
setIsLoading,
|
||||
setIsIdle,
|
||||
replaceByFee,
|
||||
stacksNetwork,
|
||||
setActiveTabActivity,
|
||||
onClose,
|
||||
setIsIdle,
|
||||
externalTxid,
|
||||
doSetLatestNonce,
|
||||
analytics,
|
||||
doChangeScreen,
|
||||
setActiveTabActivity,
|
||||
refreshAccountData,
|
||||
setLocalTxs,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { useCurrentAccountFilteredMempoolTransactionsState } from '@query/mempool/mempool.hooks';
|
||||
import { useAccountConfirmedTransactions } from '@store/accounts/account.hooks';
|
||||
|
||||
export function useCurrentAccountTxIds() {
|
||||
function useCurrentAccountTxs() {
|
||||
const confirmedTxs = useAccountConfirmedTransactions();
|
||||
const mempoolTxs = useCurrentAccountFilteredMempoolTransactionsState();
|
||||
return [...new Set([...confirmedTxs, ...mempoolTxs].map(tx => tx.tx_id))];
|
||||
return [...new Set([...confirmedTxs, ...mempoolTxs])];
|
||||
}
|
||||
|
||||
export function useCurrentAccountTxIds() {
|
||||
const currentAccountTxs = useCurrentAccountTxs();
|
||||
return currentAccountTxs.map(tx => tx.tx_id);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@ import { Unlock } from '@pages/unlock';
|
||||
import { Home } from '@pages/home/home';
|
||||
import { useUpdateLastSeenStore } from '@store/wallet/wallet.hooks';
|
||||
import { SignOutConfirmDrawer } from '@pages/sign-out-confirm/sign-out-confirm';
|
||||
import { AllowDiagnosticsDrawer } from '@pages/allow-diagnostics/allow-diagnostics';
|
||||
import { useAnalytics } from '@common/hooks/analytics/use-analytics';
|
||||
import { useHasAllowedDiagnostics } from '@store/onboarding/onboarding.hooks';
|
||||
import { AllowDiagnosticsFullPage } from '@pages/allow-diagnostics/allow-diagnostics';
|
||||
|
||||
interface RouteProps {
|
||||
path: ScreenPaths;
|
||||
@@ -45,10 +47,12 @@ export const Routes: React.FC = () => {
|
||||
const setLastSeen = useUpdateLastSeenStore();
|
||||
|
||||
const doChangeScreen = useChangeScreen();
|
||||
const analytics = useAnalytics();
|
||||
useSaveAuthRequest();
|
||||
|
||||
const isSignedIn = signedIn && !isOnboardingInProgress;
|
||||
const isLocked = !signedIn && encryptedSecretKey;
|
||||
const [hasAllowedDiagnostics, _] = useHasAllowedDiagnostics();
|
||||
|
||||
// Keep track of 'last seen' by updating it whenever a route is set.
|
||||
useEffect(() => {
|
||||
@@ -56,7 +60,12 @@ export const Routes: React.FC = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
void analytics.page('view', `${pathname}`);
|
||||
}, [analytics, pathname]);
|
||||
|
||||
const getHomeComponent = useCallback(() => {
|
||||
if (hasAllowedDiagnostics === undefined) return <AllowDiagnosticsFullPage />;
|
||||
if (isSignedIn || encryptedSecretKey) {
|
||||
return (
|
||||
<AccountGate>
|
||||
@@ -65,7 +74,7 @@ export const Routes: React.FC = () => {
|
||||
);
|
||||
}
|
||||
return <Installed />;
|
||||
}, [isSignedIn, encryptedSecretKey]);
|
||||
}, [hasAllowedDiagnostics, isSignedIn, encryptedSecretKey]);
|
||||
|
||||
const getSignInComponent = () => {
|
||||
if (isLocked) return <Unlock />;
|
||||
@@ -91,7 +100,6 @@ export const Routes: React.FC = () => {
|
||||
<RoutesDom>
|
||||
<Route path={ScreenPaths.HOME} element={getHomeComponent()}>
|
||||
<Route path={ScreenPaths.SIGN_OUT_CONFIRM} element={<SignOutConfirmDrawer />} />
|
||||
<Route path={ScreenPaths.REQUEST_DIAGNOSTICS} element={<AllowDiagnosticsDrawer />} />
|
||||
</Route>
|
||||
{/* Installation */}
|
||||
<Route path={ScreenPaths.SIGN_IN_INSTALLED} element={<InstalledSignIn />} />
|
||||
|
||||
@@ -20,7 +20,7 @@ export function useCurrentAccountLocalTxids() {
|
||||
: [];
|
||||
}
|
||||
|
||||
function useCurrentAccountLocallySubmittedStacksTransactions() {
|
||||
export function useCurrentAccountLocallySubmittedStacksTransactions() {
|
||||
const localTxs = useAtomValue(currentAccountLocallySubmittedTxsState);
|
||||
const txids = useCurrentAccountLocalTxids();
|
||||
const result: any = {};
|
||||
|
||||
@@ -31,7 +31,10 @@ export const authRequestState = atom<AuthRequestState>({
|
||||
|
||||
export const userHasAllowedDiagnosticsKey = 'stacks-wallet-has-allowed-diagnostics';
|
||||
|
||||
export const hasAllowedDiagnosticsState = atomWithStorage(userHasAllowedDiagnosticsKey, false);
|
||||
export const hasAllowedDiagnosticsState = atomWithStorage<boolean | undefined>(
|
||||
userHasAllowedDiagnosticsKey,
|
||||
undefined
|
||||
);
|
||||
|
||||
magicRecoveryCodePasswordState.debugLabel = 'magicRecoveryCodePasswordState';
|
||||
seedInputState.debugLabel = 'seedInputState';
|
||||
|
||||
@@ -22,6 +22,7 @@ describe(`Authentication integration tests`, () => {
|
||||
});
|
||||
|
||||
it('should be able to sign up from authentication page', async () => {
|
||||
await wallet.clickAllowAnalytics();
|
||||
await wallet.clickSignUp();
|
||||
await wallet.saveKey();
|
||||
await wallet.waitForHomePage();
|
||||
@@ -32,6 +33,7 @@ describe(`Authentication integration tests`, () => {
|
||||
});
|
||||
|
||||
it('should be able to login from authentication page then logout', async () => {
|
||||
await wallet.clickAllowAnalytics();
|
||||
await wallet.clickSignIn();
|
||||
await wallet.loginWithPreviousSecretKey(SECRET_KEY);
|
||||
await wallet.waitForHomePage();
|
||||
|
||||
@@ -19,6 +19,7 @@ describe(`Wallet Balance integration tests`, () => {
|
||||
beforeAll(async () => {
|
||||
browser = await setupBrowser();
|
||||
wallet = await WalletPage.init(browser, ScreenPaths.INSTALLED);
|
||||
await wallet.clickAllowAnalytics();
|
||||
await wallet.signIn(SECRET_KEY_2);
|
||||
await selectTestNet(wallet);
|
||||
await wallet.waitForHomePage();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum InitialPageSelectors {
|
||||
SignUp = 'sign-up',
|
||||
SignIn = 'sign-in',
|
||||
AnalyticsAllow = 'analytics-allow',
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ describe(`Send tokens flow`, () => {
|
||||
beforeEach(async () => {
|
||||
browser = await setupBrowser();
|
||||
walletPage = await WalletPage.init(browser, ScreenPaths.INSTALLED);
|
||||
await walletPage.clickAllowAnalytics();
|
||||
await walletPage.signUp();
|
||||
await walletPage.waitForHomePage();
|
||||
await walletPage.goToSendForm();
|
||||
@@ -100,6 +101,7 @@ describe('Preview for sending token', () => {
|
||||
beforeEach(async () => {
|
||||
browser = await setupBrowser();
|
||||
walletPage = await WalletPage.init(browser, ScreenPaths.INSTALLED);
|
||||
await walletPage.clickAllowAnalytics();
|
||||
await walletPage.signIn(SECRET_KEY_2);
|
||||
await walletPage.waitForHomePage();
|
||||
await walletPage.goToSendForm();
|
||||
|
||||
@@ -26,6 +26,7 @@ describe(`Copy Address`, () => {
|
||||
});
|
||||
|
||||
it('should be able to copy address', async () => {
|
||||
await wallet.clickAllowAnalytics();
|
||||
await wallet.signUp();
|
||||
await wallet.page.click(createTestSelector(UserAreaSelectors.AccountBalancesCopyAddress));
|
||||
let copiedAddress = await readClipboard();
|
||||
|
||||
@@ -25,6 +25,7 @@ describe(`Create and switch account`, () => {
|
||||
});
|
||||
|
||||
it('should be able to create a new account then switch', async () => {
|
||||
await wallet.clickAllowAnalytics();
|
||||
await wallet.signUp();
|
||||
await wallet.clickSettingsButton();
|
||||
await wallet.page.click(wallet.$createAccountButton);
|
||||
@@ -47,6 +48,7 @@ describe(`Create and switch account`, () => {
|
||||
});
|
||||
|
||||
it(`should be able to create ${numOfAccountsToTest} new accounts then switch between them`, async () => {
|
||||
await wallet.clickAllowAnalytics();
|
||||
await wallet.signUp();
|
||||
for (let i = 0; i < numOfAccountsToTest - 1; i++) {
|
||||
await wallet.clickSettingsButton();
|
||||
|
||||
@@ -15,6 +15,7 @@ describe(`Settings integration tests`, () => {
|
||||
beforeAll(async () => {
|
||||
browser = await setupBrowser();
|
||||
wallet = await WalletPage.init(browser, ScreenPaths.INSTALLED);
|
||||
await wallet.clickAllowAnalytics();
|
||||
await wallet.signUp();
|
||||
}, BEFORE_ALL_TIMEOUT);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ describe(`Transactions integration tests`, () => {
|
||||
browser = await setupBrowser();
|
||||
wallet = await WalletPage.init(browser, ScreenPaths.INSTALLED);
|
||||
demo = browser.demo;
|
||||
await wallet.clickAllowAnalytics();
|
||||
await wallet.signUp();
|
||||
await demo.page.reload();
|
||||
// Pattern for opening popup in new page for testing
|
||||
|
||||
@@ -17,6 +17,7 @@ export class WalletPage {
|
||||
static url = 'http://localhost:8081/index.html#';
|
||||
$signUpButton = createTestSelector(InitialPageSelectors.SignUp);
|
||||
$signInButton = createTestSelector(InitialPageSelectors.SignIn);
|
||||
$analyticsAllowButton = createTestSelector('analytics-allow');
|
||||
homePage = createTestSelector('home-page');
|
||||
$textareaReadOnlySeedPhrase = `${createTestSelector('textarea-seed-phrase')}[data-loaded="true"]`;
|
||||
$buttonSignInKeyContinue = createTestSelector('sign-in-key-continue');
|
||||
@@ -61,6 +62,9 @@ export class WalletPage {
|
||||
return new this(page);
|
||||
}
|
||||
|
||||
async clickAllowAnalytics() {
|
||||
await this.page.click(this.$analyticsAllowButton);
|
||||
}
|
||||
async clickSignUp() {
|
||||
await this.page.click(this.$signUpButton);
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@ const config = {
|
||||
|
||||
new webpack.EnvironmentPlugin({
|
||||
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
|
||||
SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY ?? '',
|
||||
}),
|
||||
|
||||
new webpack.ProvidePlugin({
|
||||
|
||||
Reference in New Issue
Block a user