diff --git a/declarations.d.ts b/declarations.d.ts index a7bf0794e..e11b87ce3 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -11,8 +11,6 @@ declare module '@env' { export const BRAZE_API_KEY_ANDROID: string; export const BRAZE_API_KEY_IOS: string; export const BRAZE_EXPORT_API_KEY: string; - export const BRAZE_MERGE_AND_DELETE_API_KEY: string; - export const BRAZE_REST_API_ENDPOINT: string; export const COINBASE_CLIENT_ID: string; export const COINBASE_CLIENT_SECRET: string; export const DISABLE_DEVELOPMENT_LOGGING: string; diff --git a/src/lib/Braze/index.ts b/src/lib/Braze/index.ts index 595174dd3..fffaee3f7 100644 --- a/src/lib/Braze/index.ts +++ b/src/lib/Braze/index.ts @@ -1,11 +1,8 @@ import Braze from '@braze/react-native-sdk'; -import axios from 'axios'; import { BRAZE_API_ENDPOINT, BRAZE_API_KEY_ANDROID, BRAZE_API_KEY_IOS, - BRAZE_MERGE_AND_DELETE_API_KEY, - BRAZE_REST_API_ENDPOINT, } from '@env'; import {checkNotifications, RESULTS} from 'react-native-permissions'; import {NativeModules, Platform} from 'react-native'; @@ -105,52 +102,6 @@ const setUserAttributes = (attributes: BrazeUserAttributes) => { }); }; -const mergeUsers = async ( - user_to_merge: string, - user_to_keep: string, -): Promise => { - const url = 'https://' + BRAZE_REST_API_ENDPOINT + '/users/merge'; - const body = { - merge_updates: [ - { - identifier_to_merge: { - external_id: user_to_merge, - }, - identifier_to_keep: { - external_id: user_to_keep, - }, - }, - ], - }; - const headers = { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + BRAZE_MERGE_AND_DELETE_API_KEY, - }; - try { - const {data} = await axios.post(url, body, {headers}); - return data; - } catch (err: any) { - throw err.response?.data?.message || err.message || err; - } -}; - -const deleteUser = async (eid: string): Promise => { - const url = 'https://' + BRAZE_REST_API_ENDPOINT + '/users/delete'; - const body = { - external_ids: [eid], - }; - const headers = { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + BRAZE_MERGE_AND_DELETE_API_KEY, - }; - try { - const {data} = await axios.post(url, body, {headers}); - return data; - } catch (err: any) { - throw err.response?.data?.message || err.message || err; - } -}; - export type BrazeUserAttributes = { [K in (typeof nonCustomAttributes)[number]]?: string; } & Record; @@ -258,14 +209,6 @@ class BrazeClientWrapper { }; } - merge(userToMerge: string, userToKeep: string) { - return mergeUsers(userToMerge, userToKeep); - } - - delete(eid: string) { - return deleteUser(eid); - } - async screen(name: string, properties: Record = {}) { if (!(await this.ensureReady())) { return; diff --git a/src/store/bitpay-id/bitpay-id.effects.spec.ts b/src/store/bitpay-id/bitpay-id.effects.spec.ts index 340feb01e..d3ede64e3 100644 --- a/src/store/bitpay-id/bitpay-id.effects.spec.ts +++ b/src/store/bitpay-id/bitpay-id.effects.spec.ts @@ -62,7 +62,6 @@ jest.mock('../../lib/Mixpanel', () => ({ jest.mock('../../lib/Braze', () => ({ BrazeWrapper: { - merge: jest.fn(() => Promise.resolve()), identify: jest.fn(() => Promise.resolve()), setEmail: jest.fn(), setEmailNotificationSubscriptionType: jest.fn(), @@ -150,17 +149,19 @@ jest.mock('../../api/bitpay', () => ({ import AuthApi from '../../api/auth'; import UserApi from '../../api/user'; +import BitPayIdApi from '../../api/bitpay'; import {getPasskeyStatus, signInWithPasskey} from '../../utils/passkey'; import * as helperMethods from '../../utils/helper-methods'; import {isAnonymousBrazeEid} from '../app/app.effects'; -import {BrazeWrapper} from '../../lib/Braze'; +import {Analytics} from '../analytics/analytics.effects'; const MockAuthApi = AuthApi as jest.Mocked; const MockUserApi = UserApi as jest.Mocked; const MockGetPasskeyStatus = getPasskeyStatus as jest.Mock; const MockSignInWithPasskey = signInWithPasskey as jest.Mock; const MockIsAnonymousBrazeEid = isAnonymousBrazeEid as jest.Mock; -const MockBrazeWrapperMerge = BrazeWrapper.merge as jest.Mock; +const MockBitPayIdApiCall = BitPayIdApi.apiCall as jest.Mock; +const MockAnalyticsEndMergingUser = Analytics.endMergingUser as jest.Mock; const MockSleep = jest .spyOn(helperMethods, 'sleep') .mockResolvedValue(undefined); @@ -288,16 +289,16 @@ describe('startBitPayIdAnalyticsInit', () => { it('does nothing when user is falsy', async () => { const store = baseStore(); await store.dispatch(startBitPayIdAnalyticsInit(null as any)); - expect(MockBrazeWrapperMerge).not.toHaveBeenCalled(); + expect(MockBitPayIdApiCall).not.toHaveBeenCalled(); }); - it('calls BrazeWrapper.merge when brazeEid is anonymous and differs from user eid', async () => { + it('calls the mergeBrazeUser API when brazeEid is anonymous and differs from user eid', async () => { MockIsAnonymousBrazeEid.mockReturnValueOnce(true); const store = configureTestStore({ BITPAY_ID: { session: makeSession(), - apiToken: {[Network.mainnet]: ''}, + apiToken: {[Network.mainnet]: 'token1'}, }, APP: { network: Network.mainnet, @@ -310,14 +311,17 @@ describe('startBitPayIdAnalyticsInit', () => { const user = makeUser({eid: 'new-eid-xyz'}); await store.dispatch(startBitPayIdAnalyticsInit(user)); - expect(MockBrazeWrapperMerge).toHaveBeenCalledWith( - 'old-anon-eid', - 'new-eid-xyz', + expect(MockBitPayIdApiCall).toHaveBeenCalledWith( + 'token1', + 'mergeBrazeUser', + { + userToMerge: 'old-anon-eid', + }, ); expect(MockSleep).toHaveBeenCalledWith(5000); }); - it('does NOT call BrazeWrapper.merge when brazeEid is not anonymous', async () => { + it('does NOT call the mergeBrazeUser API when brazeEid is not anonymous', async () => { MockIsAnonymousBrazeEid.mockReturnValueOnce(false); const store = configureTestStore({ @@ -334,7 +338,33 @@ describe('startBitPayIdAnalyticsInit', () => { }); await store.dispatch(startBitPayIdAnalyticsInit(makeUser())); - expect(MockBrazeWrapperMerge).not.toHaveBeenCalled(); + expect(MockBitPayIdApiCall).not.toHaveBeenCalled(); + }); + + it('still completes the merging lifecycle (sleep + endMergingUser) when mergeBrazeUser fails', async () => { + MockIsAnonymousBrazeEid.mockReturnValueOnce(true); + MockBitPayIdApiCall.mockRejectedValueOnce(new Error('merge failed')); + + const store = configureTestStore({ + BITPAY_ID: { + session: makeSession(), + apiToken: {[Network.mainnet]: 'token1'}, + }, + APP: { + network: Network.mainnet, + brazeEid: 'old-anon-eid', + emailNotifications: {accepted: false}, + notificationsAccepted: false, + }, + }); + + const user = makeUser({eid: 'new-eid-xyz'}); + await expect( + store.dispatch(startBitPayIdAnalyticsInit(user)), + ).resolves.toBeUndefined(); + + expect(MockSleep).toHaveBeenCalledWith(5000); + expect(MockAnalyticsEndMergingUser).toHaveBeenCalled(); }); it('derives givenName/familyName from name when they are missing', async () => { diff --git a/src/store/bitpay-id/bitpay-id.effects.ts b/src/store/bitpay-id/bitpay-id.effects.ts index f5f2c32ea..dbe53b303 100644 --- a/src/store/bitpay-id/bitpay-id.effects.ts +++ b/src/store/bitpay-id/bitpay-id.effects.ts @@ -9,7 +9,6 @@ import {BasicUserInfo, InitialUserData} from '../../api/user/user.types'; import {Network} from '../../constants'; import Dosh from '../../lib/dosh'; import {MixpanelWrapper} from '../../lib/Mixpanel'; -import {BrazeWrapper} from '../../lib/Braze'; import {isAxiosError, isRateLimitError} from '../../utils/axios'; import {generateSalt, hashPassword} from '../../utils/password'; import {Analytics} from '../analytics/analytics.effects'; @@ -45,6 +44,27 @@ interface StartLoginParams { gCaptchaResponse?: string; } +export const mergeBrazeUser = + (userToMerge: string): Effect> => + async (dispatch, getState) => + (async () => { + try { + const {APP, BITPAY_ID} = getState(); + await BitPayIdApi.apiCall( + BITPAY_ID.apiToken[APP.network], + 'mergeBrazeUser', + {userToMerge}, + ); + } catch (err: any) { + const errMsg = err instanceof Error ? err.message : JSON.stringify(err); + logManager.error( + '[mergeBrazeUser] Failed to merge Braze user.', + errMsg, + ); + throw err; + } + })(); + export const startBitPayIdAnalyticsInit = (user: BasicUserInfo): Effect => async (dispatch, getState) => { @@ -91,7 +111,7 @@ export const startBitPayIdAnalyticsInit = previousBrazeEid, eid, ); - await BrazeWrapper.merge(previousBrazeEid, eid); + await dispatch(mergeBrazeUser(previousBrazeEid)); } catch (error) { const errMsg = error instanceof Error ? error.message : JSON.stringify(error);