Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 0 additions & 57 deletions src/lib/Braze/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -105,52 +102,6 @@ const setUserAttributes = (attributes: BrazeUserAttributes) => {
});
};

const mergeUsers = async (
user_to_merge: string,
user_to_keep: string,
): Promise<any> => {
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<any> => {
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<string, any>;
Expand Down Expand Up @@ -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<string, any> = {}) {
if (!(await this.ensureReady())) {
return;
Expand Down
52 changes: 41 additions & 11 deletions src/store/bitpay-id/bitpay-id.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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<typeof AuthApi>;
const MockUserApi = UserApi as jest.Mocked<typeof UserApi>;
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);
Expand Down Expand Up @@ -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,
Expand All @@ -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({
Expand All @@ -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 () => {
Expand Down
24 changes: 22 additions & 2 deletions src/store/bitpay-id/bitpay-id.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -45,6 +44,27 @@ interface StartLoginParams {
gCaptchaResponse?: string;
}

export const mergeBrazeUser =
(userToMerge: string): Effect<Promise<void>> =>
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<void> =>
async (dispatch, getState) => {
Expand Down Expand Up @@ -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);
Expand Down
Loading