diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 520155418..170fbd44b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -286,12 +286,12 @@ PODS: - React - RNGestureHandler (2.3.0): - React-Core - - segment-analytics-react-native (2.1.12): + - segment-analytics-react-native (2.2.0): - React-Core - sovran-react-native - segment-analytics-react-native-plugin-idfa (0.2.1): - React-Core - - sovran-react-native (0.2.7): + - sovran-react-native (0.2.8): - React-Core - Yoga (1.14.0) @@ -459,9 +459,9 @@ SPEC CHECKSUMS: RNCAsyncStorage: b49b4e38a1548d03b74b30e558a1d18465b94be7 RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489 RNGestureHandler: 77d59828d40838c9fabb76a12d2d0a80c006906f - segment-analytics-react-native: 5287504fa5aa60e64dbb497bee5c7eb6f94e5e49 + segment-analytics-react-native: d0b24d7b7e6e6968a3558a2c41f61e94420b6797 segment-analytics-react-native-plugin-idfa: 80e5d610f537156833eabea12a1804523355de95 - sovran-react-native: 8d549886ad24ab51f8d471a7db83d1a3ace36358 + sovran-react-native: e4064b633fd8232055d003460d5816dff87ba8cc Yoga: 90dcd029e45d8a7c1ff059e8b3c6612ff409061a PODFILE CHECKSUM: 0c7eb82d495ca56953c50916b7b49e7512632eb6 diff --git a/example/package.json b/example/package.json index 6e4bf1274..6d15eb5ae 100644 --- a/example/package.json +++ b/example/package.json @@ -24,7 +24,7 @@ "@react-native-community/masked-view": "^0.1.11", "@react-navigation/native": "^6.0.2", "@react-navigation/stack": "^6.0.7", - "@segment/sovran-react-native": "^0.2.7", + "@segment/sovran-react-native": "^0.2.8", "react": "17.0.2", "react-native": "0.67.3", "react-native-bootsplash": "^3.2.4", diff --git a/example/yarn.lock b/example/yarn.lock index bc0efc90c..dfd169649 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1575,10 +1575,10 @@ color "^3.1.3" warn-once "^0.1.0" -"@segment/sovran-react-native@^0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.7.tgz#5df47d00a862481ab1f3f07bcc4b8e0a737930ae" - integrity sha512-P4pv3yIbUMv1X54TGioZb+8m4DiDCyKBRuyheqbFEblZeZSckI+WuRp/pooQeyHoGBMQbOY/j0yKksY3Yydkvg== +"@segment/sovran-react-native@^0.2.8": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.8.tgz#76a3c29011f9726a0fa2ac3942fb1d7715816d7e" + integrity sha512-b3a2vfEj2+jb8w/o+rNrJESWUhHEtrRZgydRNg1PEmMDlLeh42T3mDAap4mtGFICRDHU57w2zPeuw+wfs/sk7g== dependencies: "@react-native-async-storage/async-storage" "^1.15.15" ansi-regex "5.0.1" diff --git a/packages/core/package.json b/packages/core/package.json index 31358cd03..e92651427 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -46,7 +46,7 @@ "homepage": "https://github.com/segmentio/analytics-react-native#readme", "dependencies": { "@react-native-async-storage/async-storage": "^1.15.17", - "@segment/sovran-react-native": "^0.2.7", + "@segment/sovran-react-native": "^0.2.8", "deepmerge": "^4.2.2", "js-base64": "^3.7.2", "nanoid": "^3.1.25" diff --git a/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts b/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts index c5f3c9ccd..208ae193f 100644 --- a/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts +++ b/packages/core/src/__tests__/__helpers__/mockSegmentStore.ts @@ -8,6 +8,7 @@ import type { SegmentEvent, UserInfoState, } from '../../types'; +import { createCallbackManager } from './utils'; type Data = { isReady: boolean; @@ -51,35 +52,12 @@ export class MockSegmentStore implements Storage { ); } - // Callbacks - private createCallbackManager = () => { - type Callback = (value: V) => R; - const callbacks: Callback[] = []; - - const deregister = (callback: Callback) => { - callbacks.splice(callbacks.indexOf(callback), 1); - }; - - const register = (callback: Callback) => { - callbacks.push(callback); - return () => { - deregister(callback); - }; - }; - - const run = (value: V) => { - callbacks.forEach((callback) => callback(value)); - }; - - return { register, deregister, run }; - }; - private callbacks = { - context: this.createCallbackManager | undefined>(), - settings: this.createCallbackManager(), - events: this.createCallbackManager(), - userInfo: this.createCallbackManager(), - deepLinkData: this.createCallbackManager(), + context: createCallbackManager | undefined>(), + settings: createCallbackManager(), + events: createCallbackManager(), + userInfo: createCallbackManager(), + deepLinkData: createCallbackManager(), }; readonly isReady = { diff --git a/packages/core/src/__tests__/__helpers__/utils.ts b/packages/core/src/__tests__/__helpers__/utils.ts new file mode 100644 index 000000000..70019230a --- /dev/null +++ b/packages/core/src/__tests__/__helpers__/utils.ts @@ -0,0 +1,23 @@ +export const createCallbackManager = () => { + type Callback = (value: V) => R; + const callbacks: Callback[] = []; + + const deregister = (callback: Callback) => { + callbacks.splice(callbacks.indexOf(callback), 1); + }; + + const register = (callback: Callback) => { + callbacks.push(callback); + return () => { + deregister(callback); + }; + }; + + const run = (value: V) => { + for (const callback of [...callbacks]) { + callback(value); + } + }; + + return { register, deregister, run }; +}; diff --git a/packages/core/src/storage/__tests__/sovranStorage.test.ts b/packages/core/src/storage/__tests__/sovranStorage.test.ts new file mode 100644 index 000000000..feb6837ac --- /dev/null +++ b/packages/core/src/storage/__tests__/sovranStorage.test.ts @@ -0,0 +1,102 @@ +import deepmerge from 'deepmerge'; +import { createCallbackManager } from '../../__tests__/__helpers__/utils'; +import { SovranStorage } from '../sovranStorage'; + +jest.mock('@segment/sovran-react-native', () => ({ + registerBridgeStore: jest.fn(), + createStore: (initialState: T) => { + const callbackManager = createCallbackManager(); + + let store = { + ...initialState, + }; + + return { + subscribe: jest + .fn() + .mockImplementation((callback: (state: T) => void) => { + callbackManager.register(callback); + return () => callbackManager.deregister(callback); + }), + dispatch: jest + .fn() + .mockImplementation( + async (action: (state: T) => T | Promise): Promise => { + store = await action(store); + callbackManager.run(store); + return store; + } + ), + getState: jest.fn().mockImplementation(() => ({ ...store })), + }; + }, +})); + +describe('sovranStorage', () => { + it('works', async () => { + // First test that the constructor works correctly + const sovran = new SovranStorage('test'); + expect(sovran.isReady.get()).toBe(false); + + // Setup a listener for context changes + const contextListener = jest.fn(); + sovran.context.onChange(contextListener); + + // A basic test that sets up the context data in the store and checks that the listener is called + const appContext = { + app: { + name: 'test', + namespace: 'com.segment', + version: '1.0.0', + }, + device: { + manufacturer: 'Apple', + model: 'iPhone X', + name: 'iPhone', + type: 'mobile', + }, + }; + + const newContext = await sovran.context.set(appContext); + expect(newContext).toEqual(appContext); + expect(sovran.context.get()).toEqual(appContext); + expect(contextListener).toHaveBeenCalledWith(appContext); + + // Context should be deeply merged to preserve values set by other plugins + const deviceToken = { + device: { + token: '123', + }, + }; + + const expected = deepmerge(appContext, deviceToken); + const updated = await sovran.context.set(deviceToken); + expect(updated).toEqual(expected); + expect(sovran.context.get()).toEqual(expected); + expect(contextListener).toHaveBeenCalledWith(expected); + + // Now lets test the settings, settings are not deeply merged, only merged at the top level + const settings = { + segment: { + apiKey: '123', + }, + }; + + const newSettings = await sovran.settings.set(settings); + expect(newSettings).toEqual(settings); + expect(sovran.settings.get()).toEqual(settings); + + const settingsUpdate = { + segment: { + key: '123', + }, + braze: { + key: '123', + }, + }; + + const updatedSettings = await sovran.settings.set(settingsUpdate); + expect(updatedSettings).toEqual(settingsUpdate); + expect(sovran.settings.get()).toEqual(settingsUpdate); + }); +}); diff --git a/packages/core/src/storage/sovranStorage.ts b/packages/core/src/storage/sovranStorage.ts index 00c22bc73..ab660e9d7 100644 --- a/packages/core/src/storage/sovranStorage.ts +++ b/packages/core/src/storage/sovranStorage.ts @@ -3,6 +3,7 @@ import { registerBridgeStore, Store, } from '@segment/sovran-react-native'; +import deepmerge from 'deepmerge'; import type { SegmentAPIIntegrations, IntegrationSettings, @@ -162,7 +163,7 @@ export class SovranStorage implements Storage { this.contextStore.subscribe((store) => callback(store.context)), set: async (value: DeepPartial) => { const { context } = await this.contextStore.dispatch((state) => { - return { context: { ...state.context, ...value } }; + return { context: deepmerge(state.context, value) }; }); return context; }, diff --git a/yarn.lock b/yarn.lock index 6c0565ffc..e2b2f2dec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2141,10 +2141,10 @@ conventional-recommended-bump "^6.1.0" prepend-file "^2.0.0" -"@segment/sovran-react-native@^0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.7.tgz#5df47d00a862481ab1f3f07bcc4b8e0a737930ae" - integrity sha512-P4pv3yIbUMv1X54TGioZb+8m4DiDCyKBRuyheqbFEblZeZSckI+WuRp/pooQeyHoGBMQbOY/j0yKksY3Yydkvg== +"@segment/sovran-react-native@^0.2.8": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.2.8.tgz#76a3c29011f9726a0fa2ac3942fb1d7715816d7e" + integrity sha512-b3a2vfEj2+jb8w/o+rNrJESWUhHEtrRZgydRNg1PEmMDlLeh42T3mDAap4mtGFICRDHU57w2zPeuw+wfs/sk7g== dependencies: "@react-native-async-storage/async-storage" "^1.15.15" ansi-regex "5.0.1"