Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NOTIFY-1096): add account syncing #11190

Merged
merged 81 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 73 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
631dbe9
feat(NOTIFY-1096): add account syncing
mathieuartu Sep 13, 2024
04780e0
fix: import path
mathieuartu Sep 13, 2024
771ad50
fix: useEffect
mathieuartu Sep 13, 2024
2f20cdb
fix: temporarily remove keyring-controller patch
mathieuartu Sep 13, 2024
249a1ba
fix: yarn.lock
mathieuartu Sep 13, 2024
6e2b5b5
fix: remove Gemfile.lock
mathieuartu Sep 13, 2024
888f01d
fix: add @ts-expect-error for controller version mismatch
mathieuartu Sep 13, 2024
b9bb37a
fix: remove unused import
mathieuartu Sep 13, 2024
6c9b2ce
feat: group profile-sync effects under a layout effect + AppState cha…
mathieuartu Sep 13, 2024
80af81c
add layouteffect deps
mathieuartu Sep 13, 2024
ab17435
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 13, 2024
95563d9
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 16, 2024
c8e8e91
chore: remove preview packages and use latest deps
mathieuartu Sep 16, 2024
c3a3cb6
fix: yarn.lock
mathieuartu Sep 16, 2024
c0be57e
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 16, 2024
9aca646
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 17, 2024
8efc079
fix: update package.json & yarn.lock
mathieuartu Sep 17, 2024
8836b0f
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 17, 2024
970f65e
fix: update yarn.lock
mathieuartu Sep 18, 2024
63708ef
feat: add analytics
mathieuartu Sep 18, 2024
b6d891e
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 18, 2024
c821134
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 19, 2024
5d17b5d
fix: yarn.lock
mathieuartu Sep 19, 2024
b7af253
fix: bump snaps packages
mathieuartu Sep 19, 2024
91fa607
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 19, 2024
01d2304
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 19, 2024
a3fc2cc
fix: remove unused ts-error statements
mathieuartu Sep 19, 2024
a971856
fix: snaps imports
mathieuartu Sep 20, 2024
fd5615d
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 20, 2024
cdca605
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 20, 2024
92d9839
fix: yarn.lock
mathieuartu Sep 20, 2024
40e8294
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 23, 2024
c157056
fix: yarn.lock
mathieuartu Sep 23, 2024
5d708da
fix: yarn.lock
mathieuartu Sep 23, 2024
77c9c31
Merge branch 'main' into feat/add_account_syncing
mathieuartu Sep 25, 2024
0210192
fix: yarn.lock
mathieuartu Sep 25, 2024
2be206f
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 3, 2024
f5a2aa9
fix: bump profile-sync-controller version
mathieuartu Oct 3, 2024
1050e86
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 7, 2024
66aff4c
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 8, 2024
22cfc1a
fix: update analytics events names
mathieuartu Oct 9, 2024
bcfd1bb
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 9, 2024
307b449
Merge branch 'feat/add_account_syncing' of github.com:MetaMask/metama…
mathieuartu Oct 9, 2024
7a4dc64
fix: update yarn.lock
mathieuartu Oct 9, 2024
b03fd6a
fix: build issues
mathieuartu Oct 9, 2024
ddf4c9e
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 15, 2024
2fee02f
fix: update yarn.lock
mathieuartu Oct 15, 2024
0fa03b7
fix: yarn dedupe
mathieuartu Oct 15, 2024
730fb38
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 15, 2024
c953e4f
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 16, 2024
1ba5618
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 16, 2024
5752f14
fix: add missing semi-colon
mathieuartu Oct 16, 2024
d859175
fix: move account syncing to its own hook file
mathieuartu Oct 16, 2024
250f1e9
fix: add tests
mathieuartu Oct 17, 2024
b0786f6
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 17, 2024
3885490
feat: use new event builder
mathieuartu Oct 17, 2024
9ca93ba
fix: formatting
mathieuartu Oct 17, 2024
d718861
fix: PR feedbacks
mathieuartu Oct 18, 2024
0736b84
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 18, 2024
e4cd362
fix: update yarn.lock
mathieuartu Oct 18, 2024
ee8993b
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 23, 2024
e3147a8
feat: (WIP) add first E2E tests
mathieuartu Oct 23, 2024
cf34a7f
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 23, 2024
84d4e40
fix: refine mock system & user storage
mathieuartu Oct 25, 2024
9f2b709
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 25, 2024
3ed2263
fix: change folder structure and move ts files back to js
mathieuartu Oct 25, 2024
202e486
feat: disable account sync in production
mathieuartu Oct 25, 2024
2ab0e9f
feat: add notifications tests to bitrise flow
mathieuartu Oct 25, 2024
727375c
fix: add SmokeNotifications tag
mathieuartu Oct 25, 2024
bbf6920
fix: usage of smokeNotifications tag
mathieuartu Oct 25, 2024
0c8aa2e
fix: use process.env.IS_TEST to debug bitrise failing
mathieuartu Oct 25, 2024
3f53a49
fix: bitrise config
mathieuartu Oct 25, 2024
7415f6e
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 28, 2024
5bf38f3
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 31, 2024
ab6011e
fix: use test matchers and pages
mathieuartu Oct 31, 2024
9456194
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 31, 2024
89cc06b
fix: yarn.lock
mathieuartu Oct 31, 2024
f790e56
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 31, 2024
5035497
fix: yarn.lock
mathieuartu Oct 31, 2024
ac749df
Merge branch 'main' into feat/add_account_syncing
mathieuartu Oct 31, 2024
d0ce191
fix: yarn.lock
mathieuartu Oct 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions app/actions/notification/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,28 @@ export const markMetamaskNotificationsAsRead = async (
return getErrorMessage(error);
}
};

export const syncInternalAccountsWithUserStorage = async () => {
try {
await Engine.context.UserStorageController.syncInternalAccountsWithUserStorage();
} catch (error) {
return getErrorMessage(error);
}
};

/**
* Perform the deletion of the notifications storage key and the creation of on chain triggers to reset the notifications.
*
* @returns {Promise<string | undefined>} A promise that resolves to a string error message or undefined if successful.
*/
export const performDeleteStorage = async (): Promise<string | undefined> => {
try {
await Engine.context.UserStorageController.performDeleteStorage('notifications.notification_settings');
await Engine.context.NotificationServicesController.createOnChainTriggers(
{
await Engine.context.UserStorageController.performDeleteStorage(
'notifications.notification_settings',
);
await Engine.context.NotificationServicesController.createOnChainTriggers({
resetNotifications: true,
},
);
});
} catch (error) {
return getErrorMessage(error);
}
Expand Down
47 changes: 46 additions & 1 deletion app/components/Views/Wallet/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from 'react';
import Wallet from './';
import { renderScreen } from '../../../util/test/renderWithProvider';
import { screen } from '@testing-library/react-native';
import { act, screen } from '@testing-library/react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import Routes from '../../../constants/navigation/Routes';
import { backgroundState } from '../../../util/test/initial-root-state';
import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils';
import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
import { CommonSelectorsIDs } from '../../../../e2e/selectors/Common.selectors';
import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing';
import { AppState } from 'react-native';

const MOCK_ADDRESS = '0xc4955c0d639d99699bfd7ec54d9fafee40e4d272';

Expand Down Expand Up @@ -108,6 +110,13 @@ jest.mock('react-native-scrollable-tab-view', () => {
return ScrollableTabViewMock;
});

jest.mock('../../../util/notifications/hooks/useAccountSyncing', () => ({
useAccountSyncing: jest.fn().mockReturnValue({
dispatchAccountSyncing: jest.fn(),
error: undefined,
}),
}));

const render = (Component: React.ComponentType) =>
renderScreen(
Component,
Expand Down Expand Up @@ -144,4 +153,40 @@ describe('Wallet', () => {
const foxIcon = screen.getByTestId(CommonSelectorsIDs.FOX_ICON);
expect(foxIcon).toBeDefined();
});
it('dispatches account syncing on mount', () => {
jest.clearAllMocks();
//@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar
render(Wallet);
expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(1);
});
it('dispatches account syncing when appState switches from inactive|background to active', () => {
jest.clearAllMocks();

const addEventListener = jest.spyOn(AppState, 'addEventListener');

//@ts-expect-error we are ignoring the navigation params on purpose because we do not want to mock setOptions to test the navbar
render(Wallet);

expect(addEventListener).toHaveBeenCalledWith(
'change',
expect.any(Function),
);
const handleAppStateChange = (
addEventListener as jest.Mock
).mock.calls.find(([event]) => event === 'change')[1];

act(() => {
handleAppStateChange('background');
handleAppStateChange('active');
});

expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(2);

act(() => {
handleAppStateChange('inactive');
handleAppStateChange('active');
});

expect(useAccountSyncing().dispatchAccountSyncing).toHaveBeenCalledTimes(3);
});
});
14 changes: 12 additions & 2 deletions app/components/Views/Wallet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ import {
} from '../../../selectors/notifications';
import { ButtonVariants } from '../../../component-library/components/Buttons/Button';
import { useListNotifications } from '../../../util/notifications/hooks/useNotifications';
import { useAccountSyncing } from '../../../util/notifications/hooks/useAccountSyncing';

import { PortfolioBalance } from '../../UI/Tokens/TokenList/PortfolioBalance';
import { isObject } from 'lodash';

Expand Down Expand Up @@ -171,6 +173,7 @@ const Wallet = ({
const appState = useRef(AppState.currentState);
const { navigate } = useNavigation();
const { listNotifications } = useListNotifications();
const { dispatchAccountSyncing } = useAccountSyncing();
const walletRef = useRef(null);
const theme = useTheme();
const { toastRef } = useContext(ToastContext);
Expand Down Expand Up @@ -447,13 +450,17 @@ const Wallet = ({
[navigation, providerConfig.chainId],
);

// Layout effect when component/view is visible
// - fetches notifications
// - dispatches account syncing
useLayoutEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (
appState.current.match(/inactive|background/) &&
appState.current?.match(/inactive|background/) &&
nextAppState === 'active'
) {
listNotifications();
dispatchAccountSyncing();
mathieuartu marked this conversation as resolved.
Show resolved Hide resolved
mathieuartu marked this conversation as resolved.
Show resolved Hide resolved
}

appState.current = nextAppState;
Expand All @@ -463,11 +470,14 @@ const Wallet = ({
'change',
handleAppStateChange,
);

listNotifications();
dispatchAccountSyncing();

return () => {
subscription.remove();
};
}, [listNotifications]);
}, [listNotifications, dispatchAccountSyncing]);

useEffect(() => {
navigation.setOptions(
Expand Down
9 changes: 8 additions & 1 deletion app/core/Analytics/MetaMetrics.events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ enum EVENT_NAME {
PRIMARY_CURRENCY_TOGGLE = 'primary_currency_toggle',
LOGIN_DOWNLOAD_LOGS = 'Download State Logs Button Clicked',

// Profile Syncing
ACCOUNTS_SYNC_ADDED = 'Accounts Sync Added',
ACCOUNTS_SYNC_NAME_UPDATED = 'Accounts Sync Name Updated',
// network
MULTI_RPC_MIGRATION_MODAL_ACCEPTED = 'multi_rpc_migration_modal_accepted',

Expand Down Expand Up @@ -865,7 +868,11 @@ const events = {
),
PRIMARY_CURRENCY_TOGGLE: generateOpt(EVENT_NAME.PRIMARY_CURRENCY_TOGGLE),
LOGIN_DOWNLOAD_LOGS: generateOpt(EVENT_NAME.LOGIN_DOWNLOAD_LOGS),

// Profile Syncing
ACCOUNTS_SYNC_ADDED: generateOpt(EVENT_NAME.ACCOUNTS_SYNC_ADDED),
ACCOUNTS_SYNC_NAME_UPDATED: generateOpt(
EVENT_NAME.ACCOUNTS_SYNC_NAME_UPDATED,
),
// Connection
CONNECTION_DROPPED: generateOpt(EVENT_NAME.CONNECTION_DROPPED),
CONNECTION_RESTORED: generateOpt(EVENT_NAME.CONNECTION_RESTORED),
Expand Down
37 changes: 33 additions & 4 deletions app/core/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ import {
} from '@metamask/snaps-controllers';

import { WebViewExecutionService } from '@metamask/snaps-controllers/react-native';
import { NotificationArgs } from '@metamask/snaps-rpc-methods/dist/types/restricted/notify';
import { NotificationParameters } from '@metamask/snaps-rpc-methods/dist/restricted/notify.cjs';
mathieuartu marked this conversation as resolved.
Show resolved Hide resolved
import { getSnapsWebViewPromise } from '../lib/snaps';
import {
buildSnapEndowmentSpecifications,
Expand Down Expand Up @@ -902,7 +902,7 @@ export class Engine {
type,
requestData: { content, placeholder },
}),
showInAppNotification: (origin: string, args: NotificationArgs) => {
showInAppNotification: (origin: string, args: NotificationParameters) => {
Logger.log(
'Snaps/ showInAppNotification called with args: ',
args,
Expand Down Expand Up @@ -1226,26 +1226,55 @@ export class Engine {

const userStorageController = new UserStorageController.Controller({
getMetaMetricsState: () => MetaMetrics.getInstance().isEnabled(),
env: {
isAccountSyncingEnabled: Boolean(process.env.IS_TEST),
mathieuartu marked this conversation as resolved.
Show resolved Hide resolved
},
config: {
accountSyncing: {
onAccountAdded: (profileId) => {
MetaMetrics.getInstance().trackEvent(
MetricsEventBuilder.createEventBuilder(
MetaMetricsEvents.ACCOUNTS_SYNC_ADDED,
)
.addProperties({
profile_id: profileId,
})
.build(),
);
},
onAccountNameUpdated: (profileId) => {
MetaMetrics.getInstance().trackEvent(
MetricsEventBuilder.createEventBuilder(
MetaMetricsEvents.ACCOUNTS_SYNC_NAME_UPDATED,
)
.addProperties({
profile_id: profileId,
})
.build(),
);
},
},
},
state: initialState.UserStorageController,
messenger: this.controllerMessenger.getRestricted({
name: 'UserStorageController',
allowedActions: [
'SnapController:handleRequest',
'KeyringController:getState',
'KeyringController:addNewAccount',
'AuthenticationController:getBearerToken',
'AuthenticationController:getSessionProfile',
'AuthenticationController:isSignedIn',
'AuthenticationController:performSignOut',
'AuthenticationController:performSignIn',
'NotificationServicesController:disableNotificationServices',
'NotificationServicesController:selectIsNotificationServicesEnabled',
'KeyringController:addNewAccount',
'AccountsController:listAccounts',
'AccountsController:updateAccountMetadata',
],
allowedEvents: [
'KeyringController:lock',
'KeyringController:unlock',
'KeyringController:lock',
'AccountsController:accountAdded',
'AccountsController:accountRenamed',
],
Expand Down
102 changes: 102 additions & 0 deletions app/util/notifications/hooks/useAccountSyncing.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-disable import/no-namespace */
/* eslint-disable react/prop-types */
/* eslint-disable react/display-name */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/no-require-imports */
import { act, renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { Provider } from 'react-redux';
import createMockStore from 'redux-mock-store';
import * as Actions from '../../../actions/notification/helpers';
import initialRootState from '../../../util/test/initial-root-state';
import { useAccountSyncing } from './useAccountSyncing';

function arrangeStore() {
const store = createMockStore()(initialRootState);

// Ensure dispatch mocks are handled correctly
store.dispatch = jest.fn().mockImplementation((action) => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return Promise.resolve();
});

return store;
}

describe('useAccountSyncing', () => {
beforeEach(() => {
jest.clearAllMocks();
});

function arrangeHook() {
const store = arrangeStore();
const hook = renderHook(() => useAccountSyncing(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});

return hook;
}

function arrangeActions() {
const syncInternalAccountsWithUserStorageAction = jest
.spyOn(Actions, 'syncInternalAccountsWithUserStorage')
.mockResolvedValue(undefined);

return {
syncInternalAccountsWithUserStorageAction,
};
}

it('dispatches account syncing and error as undefined', async () => {
const mockActions = arrangeActions();

const { result } = arrangeHook();
await act(async () => {
await result.current.dispatchAccountSyncing();
});

expect(
mockActions.syncInternalAccountsWithUserStorageAction,
).toHaveBeenCalledTimes(1);
expect(result.current.error).toBeUndefined();
});

it('sets error message when syncInternalAccountsWithUserStorageAction returns an error', async () => {
const mockActions = arrangeActions();
mockActions.syncInternalAccountsWithUserStorageAction.mockRejectedValueOnce(
new Error('MOCK - failed to sync internal account with user storage'),
);

const { result } = arrangeHook();
await act(async () => {
await result.current.dispatchAccountSyncing();
});

expect(
mockActions.syncInternalAccountsWithUserStorageAction,
).toHaveBeenCalledTimes(1);
expect(result.current.error).toBeDefined();
expect(result.current.error).toEqual(
'MOCK - failed to sync internal account with user storage',
);
});

it('sets error message when an error occurs during dispatchAccountSyncing', async () => {
const mockActions = arrangeActions();
mockActions.syncInternalAccountsWithUserStorageAction.mockRejectedValueOnce(
new Error('MOCK - failed to sync internal account with user storage'),
);

const { result } = arrangeHook();
await act(async () => {
await result.current.dispatchAccountSyncing();
});

expect(result.current.error).toBeDefined();
expect(result.current.error).toEqual(
'MOCK - failed to sync internal account with user storage',
);
});
});
32 changes: 32 additions & 0 deletions app/util/notifications/hooks/useAccountSyncing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState, useCallback } from 'react';
import { getErrorMessage } from '../../../util/errorHandling';
import { syncInternalAccountsWithUserStorage as syncInternalAccountsWithUserStorageAction } from '../../../actions/notification/helpers';

/**
* Custom hook to dispatch account syncing.
*
* @returns An object containing the `dispatchAccountSyncing` function, and error state.
*/
export const useAccountSyncing = () => {
const [error, setError] = useState<string>();

const dispatchAccountSyncing = useCallback(async () => {
setError(undefined);
try {
const errorMessage = await syncInternalAccountsWithUserStorageAction();
if (errorMessage) {
setError(getErrorMessage(errorMessage));
return errorMessage;
}
} catch (e) {
const errorMessage = getErrorMessage(e);
setError(errorMessage);
return errorMessage;
}
}, []);

return {
dispatchAccountSyncing,
error,
};
};
1 change: 0 additions & 1 deletion app/util/notifications/hooks/useProfileSyncing.ts
mathieuartu marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable import/prefer-default-export */
import { useState, useCallback } from 'react';
import { ProfileSyncingReturn } from './types';
import { getErrorMessage } from '../../../util/errorHandling';
Expand Down
Loading
Loading