From 2c57fae0bc4ec9bb09ef70462d1c98fec24b0352 Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Tue, 13 Jun 2023 12:41:22 +0300 Subject: [PATCH 1/3] Update README.md (#57) --- README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index 3885aa3..fe1fa07 100644 --- a/README.md +++ b/README.md @@ -68,16 +68,9 @@ Send subscription data to your favorite platforms. Share your mobile and web sub Convinced? Let's go! -## Getting Started - -1. [Create a project and register your web app](https://documentation.qonversion.io/docs/quickstart#1-create-a-project-and-register-your-app) -2. [Configure entitlements](https://documentation.qonversion.io/docs/quickstart#2-configure-products--permissions-entitlements) -3. [Install the SDK](https://documentation.qonversion.io/docs/web-sdk#install) -4. [Use all SDK features in a few lines](https://documentation.qonversion.io/docs/web-sdk#launching-the-sdk) - ## Documentation -Check the [documentation](https://docs.qonversion.io) to learn details on implementing and using Qonversion SDKs. +Check the [documentation](https://documentation.qonversion.io/docs/quickstart) to learn details on implementing and using Qonversion SDKs. #### Help us improve the documentation From 417e078d450f559f4c6aa1f0a280fbb7ae3f9595 Mon Sep 17 00:00:00 2001 From: Kamo Spertsyan Date: Mon, 14 Aug 2023 13:45:57 +0300 Subject: [PATCH 2/3] New properties v3 get and post requests supported. (#58) * New properties v3 get and post requests supported. * Fixed exports --- sdk/src/UserPropertiesBuilder.ts | 40 +++-- .../internal/QonversionInternal.test.ts | 29 +++- .../entitlements/EntitlementsService.test.ts | 10 +- .../internal/network/ApiInteractor.test.ts | 10 +- .../network/RequestConfigurator.test.ts | 30 ++-- .../purchases/PurchasesController.test.ts | 1 + .../purchases/PurchasesService.test.ts | 10 +- .../internal/user/IdentityService.test.ts | 10 +- .../internal/user/UserService.test.ts | 11 +- .../UserPropertiesController.test.ts | 61 ++++++-- .../UserPropertiesService.test.ts | 146 +++++++++++++++--- sdk/src/dto/UserProperties.ts | 76 +++++++++ sdk/src/dto/UserProperty.ts | 33 ++-- sdk/src/dto/UserPropertyKey.ts | 19 +++ sdk/src/index.ts | 4 +- sdk/src/internal/QonversionInternal.ts | 16 +- sdk/src/internal/di/ControllersAssembly.ts | 1 + sdk/src/internal/entitlements/types.ts | 1 + sdk/src/internal/network/ApiInteractor.ts | 8 +- .../internal/network/RequestConfigurator.ts | 16 +- sdk/src/internal/network/types.ts | 19 ++- sdk/src/internal/purchases/types.ts | 1 + sdk/src/internal/user/UserController.ts | 1 + .../UserPropertiesController.ts | 58 ++++--- .../userProperties/UserPropertiesService.ts | 27 +++- sdk/src/internal/userProperties/types.ts | 22 ++- sdk/src/internal/utils/propertyUtils.ts | 5 + sdk/src/types.ts | 25 ++- 28 files changed, 531 insertions(+), 159 deletions(-) create mode 100644 sdk/src/dto/UserProperties.ts create mode 100644 sdk/src/dto/UserPropertyKey.ts create mode 100644 sdk/src/internal/utils/propertyUtils.ts diff --git a/sdk/src/UserPropertiesBuilder.ts b/sdk/src/UserPropertiesBuilder.ts index bafa999..ed1471f 100644 --- a/sdk/src/UserPropertiesBuilder.ts +++ b/sdk/src/UserPropertiesBuilder.ts @@ -1,4 +1,4 @@ -import {UserProperty} from './dto/UserProperty'; +import {UserPropertyKey} from './dto/UserPropertyKey'; /** * This builder class can be used to generate a map of user properties @@ -17,10 +17,10 @@ export class UserPropertiesBuilder { * @return builder instance for chain calls */ setName(name: string): UserPropertiesBuilder { - this.properties[UserProperty.Name] = name; + this.properties[UserPropertyKey.Name] = name; return this; } - + /** * Set custom user id. It can be an identifier used on your backend * to link the current Qonversion user with your one. @@ -28,7 +28,7 @@ export class UserPropertiesBuilder { * @return builder instance for chain calls */ setCustomUserId(customUserId: string): UserPropertiesBuilder { - this.properties[UserProperty.CustomUserId] = customUserId; + this.properties[UserPropertyKey.CustomUserId] = customUserId; return this; } @@ -38,7 +38,7 @@ export class UserPropertiesBuilder { * @return builder instance for chain calls */ setEmail(email: string): UserPropertiesBuilder { - this.properties[UserProperty.Email] = email; + this.properties[UserPropertyKey.Email] = email; return this; } @@ -48,7 +48,7 @@ export class UserPropertiesBuilder { * @return builder instance for chain calls */ setKochavaDeviceId(deviceId: string): UserPropertiesBuilder { - this.properties[UserProperty.KochavaDeviceId] = deviceId; + this.properties[UserPropertyKey.KochavaDeviceId] = deviceId; return this; } @@ -59,7 +59,7 @@ export class UserPropertiesBuilder { * @return builder instance for chain calls */ setAppsFlyerUserId(userId: string): UserPropertiesBuilder { - this.properties[UserProperty.AppsFlyerUserId] = userId; + this.properties[UserPropertyKey.AppsFlyerUserId] = userId; return this; } @@ -69,7 +69,7 @@ export class UserPropertiesBuilder { * @return builder instance for chain calls */ setAdjustAdvertisingId(advertisingId: string): UserPropertiesBuilder { - this.properties[UserProperty.AdjustAdId] = advertisingId; + this.properties[UserPropertyKey.AdjustAdId] = advertisingId; return this; } @@ -79,7 +79,7 @@ export class UserPropertiesBuilder { * @return builder instance for chain calls */ setFacebookAttribution(facebookAttribution: string): UserPropertiesBuilder { - this.properties[UserProperty.FacebookAttribution] = facebookAttribution; + this.properties[UserPropertyKey.FacebookAttribution] = facebookAttribution; return this; } @@ -89,7 +89,27 @@ export class UserPropertiesBuilder { * @return builder instance for chain calls */ setFirebaseAppInstanceId(firebaseAppInstanceId: string): UserPropertiesBuilder { - this.properties[UserProperty.FirebaseAppInstanceId] = firebaseAppInstanceId; + this.properties[UserPropertyKey.FirebaseAppInstanceId] = firebaseAppInstanceId; + return this; + } + + /** + * Set Android app set id. + * @param appSetId app set id + * @return builder instance for chain calls + */ + setAppSetId(appSetId: string): UserPropertiesBuilder { + this.properties[UserPropertyKey.AppSetId] = appSetId; + return this; + } + + /** + * Set iOS advertising id. + * @param advertisingId advertising id + * @return builder instance for chain calls + */ + setAdvertisingId(advertisingId: string): UserPropertiesBuilder { + this.properties[UserPropertyKey.AdvertisingId] = advertisingId; return this; } diff --git a/sdk/src/__tests__/internal/QonversionInternal.test.ts b/sdk/src/__tests__/internal/QonversionInternal.test.ts index c618063..180b4a3 100644 --- a/sdk/src/__tests__/internal/QonversionInternal.test.ts +++ b/sdk/src/__tests__/internal/QonversionInternal.test.ts @@ -7,7 +7,7 @@ import Qonversion, { LogLevel, PurchaseCoreData, StripeStoreData, - UserProperty, + UserPropertyKey, UserPurchase, } from '../../index'; import {UserPropertiesController} from '../../internal/userProperties'; @@ -15,6 +15,9 @@ import {UserController} from '../../internal/user'; import {EntitlementsController, EntitlementsControllerImpl} from '../../internal/entitlements'; import {PurchasesController, PurchasesControllerImpl} from '../../internal/purchases'; import {Logger} from '../../internal/logger'; +import {API_URL} from '../../internal/network'; +import {UserProperties} from '../../dto/UserProperties'; +import {UserProperty} from '../../dto/UserProperty'; jest.mock('../../internal/di/DependenciesAssembly', () => { const originalModule = jest.requireActual('../../internal/di/DependenciesAssembly'); @@ -41,6 +44,7 @@ beforeEach(() => { }; networkConfig = { canSendRequests: true, + apiUrl: API_URL, }; loggerConfig = { logTag: '', @@ -190,7 +194,7 @@ describe('UserPropertiesController usage tests', () => { test('setUserProperty', () => { // given - const key = UserProperty.AppsFlyerUserId; + const key = UserPropertyKey.AppsFlyerUserId; const value = 'property_value'; userPropertyController.setProperty = jest.fn(); @@ -218,21 +222,35 @@ describe('UserPropertiesController usage tests', () => { expect(userPropertyController.setProperties).toBeCalledWith(properties); expect(logger.verbose).toBeCalledWith('setUserProperties() call'); }); + + test('userProperties', async () => { + // given + const response = new UserProperties([new UserProperty('testKey', 'testValue')]); + userPropertyController.getProperties = jest.fn(() => Promise.resolve(response)); + + // when + const res = await qonversionInternal.userProperties(); + + // then + expect(res).toEqual(response); + expect(userPropertyController.getProperties).toBeCalledWith(); + expect(logger.verbose).toBeCalledWith('userProperties() call'); + }); }); describe('EntitlementsController usage tests', () => { - test('getEntitlements', () => { + test('entitlements', () => { // given const promiseReturned = new Promise(() => []); entitlementsController.getEntitlements = jest.fn(async () => promiseReturned); // when - const res = qonversionInternal.getEntitlements(); + const res = qonversionInternal.entitlements(); // then expect(res).toStrictEqual(promiseReturned); expect(entitlementsController.getEntitlements).toBeCalled(); - expect(logger.verbose).toBeCalledWith('getEntitlements() call'); + expect(logger.verbose).toBeCalledWith('entitlements() call'); }); }); @@ -248,6 +266,7 @@ describe('PurchasesController usage tests', () => { productId: 'test product id', subscriptionId: 'test subscription id', }, + userId: 'Qon_test_user_id' }; const requestData: PurchaseCoreData & StripeStoreData = { currency: 'USD', diff --git a/sdk/src/__tests__/internal/entitlements/EntitlementsService.test.ts b/sdk/src/__tests__/internal/entitlements/EntitlementsService.test.ts index 57a35e0..2f0fc02 100644 --- a/sdk/src/__tests__/internal/entitlements/EntitlementsService.test.ts +++ b/sdk/src/__tests__/internal/entitlements/EntitlementsService.test.ts @@ -2,8 +2,8 @@ import { ApiInteractor, RequestConfigurator, NetworkRequest, - NetworkResponseError, - NetworkResponseSuccess + ApiResponseError, + ApiResponseSuccess } from '../../../internal/network'; import { Entitlement, @@ -45,14 +45,14 @@ const apiPayload: EntitlementsResponse = { data: [apiEntitlement], object: 'list' }; -const testSuccessfulResponse: NetworkResponseSuccess = { +const testSuccessfulResponse: ApiResponseSuccess = { code: 200, data: apiPayload, isSuccess: true }; const testErrorCode = 500; const testErrorMessage = 'Test error message'; -const testErrorResponse: NetworkResponseError = { +const testErrorResponse: ApiResponseError = { code: testErrorCode, apiCode: '', message: testErrorMessage, @@ -119,7 +119,7 @@ describe('getEntitlements tests', function () { test('user does not exist', async () => { // given - const testUserNotFoundResponse: NetworkResponseError = { + const testUserNotFoundResponse: ApiResponseError = { code: HTTP_CODE_NOT_FOUND, apiCode: '', message: testErrorMessage, diff --git a/sdk/src/__tests__/internal/network/ApiInteractor.test.ts b/sdk/src/__tests__/internal/network/ApiInteractor.test.ts index 3c99c5c..ab40189 100644 --- a/sdk/src/__tests__/internal/network/ApiInteractor.test.ts +++ b/sdk/src/__tests__/internal/network/ApiInteractor.test.ts @@ -4,8 +4,8 @@ import { ExponentialDelayCalculator, NetworkClientImpl, NetworkRequest, - NetworkResponseError, - NetworkResponseSuccess, + ApiResponseError, + ApiResponseSuccess, NetworkRetryConfig, RawNetworkResponse, RequestType, @@ -55,7 +55,7 @@ describe('execute tests', () => { code: testResponseCode, payload: testPayload, }; - let errorResponse: NetworkResponseError = { + let errorResponse: ApiResponseError = { apiCode: '', code: 0, message: '', @@ -92,7 +92,7 @@ describe('execute tests', () => { test('execute with successful response', async () => { // given - const expResponse: NetworkResponseSuccess = { + const expResponse: ApiResponseSuccess = { code: testResponseCode, data: testPayload, isSuccess: true, @@ -360,7 +360,7 @@ describe('getErrorResponse tests', () => { code: testErrorCode, payload: {error: apiError}, }; - const expResult: NetworkResponseError = { + const expResult: ApiResponseError = { apiCode: testErrorApiCode, code: testErrorCode, message: testErrorMessage, diff --git a/sdk/src/__tests__/internal/network/RequestConfigurator.test.ts b/sdk/src/__tests__/internal/network/RequestConfigurator.test.ts index eb48ee6..fd0d7d0 100644 --- a/sdk/src/__tests__/internal/network/RequestConfigurator.test.ts +++ b/sdk/src/__tests__/internal/network/RequestConfigurator.test.ts @@ -77,22 +77,34 @@ describe('RequestConfigurator tests', () => { expect(request).toStrictEqual(expResult); }); - test('user properties request', () => { + test('user properties send request', () => { // given - const properties = {a: 'a', b: 'b'}; + const properties = [{key: 'a', value: 'a'}, {key: 'b', value: 'b'}]; const expResult: NetworkRequest = { headers: testHeaders, type: RequestType.POST, - url: testBaseUrl + '/' + ApiEndpoint.Properties, - body: { - access_token: testProjectKey, - q_uid: testUserId, - properties, - } + url: testBaseUrl + '/' + ApiEndpoint.Users + '/' + testUserId + '/' + ApiEndpoint.Properties, + body: properties, + }; + + // when + const request = requestConfigurator.configureUserPropertiesSendRequest(testUserId, properties); + + // then + expect(request).toStrictEqual(expResult); + }); + + test('user properties get request', () => { + // given + const expResult: NetworkRequest = { + headers: testHeaders, + type: RequestType.GET, + url: testBaseUrl + '/' + ApiEndpoint.Users + '/' + testUserId + '/' + ApiEndpoint.Properties, + body: undefined, }; // when - const request = requestConfigurator.configureUserPropertiesRequest(properties); + const request = requestConfigurator.configureUserPropertiesGetRequest(testUserId); // then expect(request).toStrictEqual(expResult); diff --git a/sdk/src/__tests__/internal/purchases/PurchasesController.test.ts b/sdk/src/__tests__/internal/purchases/PurchasesController.test.ts index c7ba172..2585b5b 100644 --- a/sdk/src/__tests__/internal/purchases/PurchasesController.test.ts +++ b/sdk/src/__tests__/internal/purchases/PurchasesController.test.ts @@ -17,6 +17,7 @@ const testUserPurchase: UserPurchase = { productId: 'test product id', subscriptionId: 'test subscription id' }, + userId: testUserId, }; const testStripePurchaseData: PurchaseCoreData & StripeStoreData = { diff --git a/sdk/src/__tests__/internal/purchases/PurchasesService.test.ts b/sdk/src/__tests__/internal/purchases/PurchasesService.test.ts index 811e8d3..e90ce0d 100644 --- a/sdk/src/__tests__/internal/purchases/PurchasesService.test.ts +++ b/sdk/src/__tests__/internal/purchases/PurchasesService.test.ts @@ -2,8 +2,8 @@ import { ApiInteractor, RequestConfigurator, NetworkRequest, - NetworkResponseError, - NetworkResponseSuccess + ApiResponseError, + ApiResponseSuccess } from '../../../internal/network'; import { PurchaseCoreData, @@ -23,20 +23,21 @@ const apiPurchase: UserPurchaseApi = { currency: 'USD', price: '10', purchased: 3243523432, + userId: testUserId, stripe_store_data: { product_id: 'test product id', subscription_id: 'test subscription id' }, }; -const testSuccessfulResponse: NetworkResponseSuccess = { +const testSuccessfulResponse: ApiResponseSuccess = { code: 200, data: apiPurchase, isSuccess: true }; const testErrorCode = 500; const testErrorMessage = 'Test error message'; -const testErrorResponse: NetworkResponseError = { +const testErrorResponse: ApiResponseError = { code: testErrorCode, apiCode: '', message: testErrorMessage, @@ -47,6 +48,7 @@ const expRes: UserPurchase = { currency: 'USD', price: '10', purchased: 3243523432, + userId: testUserId, stripeStoreData: { productId: 'test product id', subscriptionId: 'test subscription id' diff --git a/sdk/src/__tests__/internal/user/IdentityService.test.ts b/sdk/src/__tests__/internal/user/IdentityService.test.ts index 0707176..817a809 100644 --- a/sdk/src/__tests__/internal/user/IdentityService.test.ts +++ b/sdk/src/__tests__/internal/user/IdentityService.test.ts @@ -1,8 +1,8 @@ import { ApiInteractor, RequestConfigurator, NetworkRequest, - NetworkResponseError, - NetworkResponseSuccess + ApiResponseError, + ApiResponseSuccess } from '../../../internal/network'; import {QonversionError, QonversionErrorCode} from '../../../index'; import {HTTP_CODE_NOT_FOUND} from '../../../internal/network/constants'; @@ -17,14 +17,14 @@ const testIdentityUserId = 'test identity user id'; const apiPayload: IdentityApi = { user_id: testQonversionUserId, }; -const testSuccessfulResponse: NetworkResponseSuccess = { +const testSuccessfulResponse: ApiResponseSuccess = { code: 200, data: apiPayload, isSuccess: true }; const testErrorCode = 500; const testErrorMessage = 'Test error message'; -const testErrorResponse: NetworkResponseError = { +const testErrorResponse: ApiResponseError = { code: testErrorCode, apiCode: '', message: testErrorMessage, @@ -77,7 +77,7 @@ describe('obtainIdentity tests', function () { test('identity does not exist', async () => { // given - const testUserNotFoundResponse: NetworkResponseError = { + const testUserNotFoundResponse: ApiResponseError = { code: HTTP_CODE_NOT_FOUND, apiCode: '', message: testErrorMessage, diff --git a/sdk/src/__tests__/internal/user/UserService.test.ts b/sdk/src/__tests__/internal/user/UserService.test.ts index 4b8d2b9..ec92281 100644 --- a/sdk/src/__tests__/internal/user/UserService.test.ts +++ b/sdk/src/__tests__/internal/user/UserService.test.ts @@ -1,8 +1,9 @@ import { ApiInteractor, RequestConfigurator, - NetworkRequest, NetworkResponseError, - NetworkResponseSuccess + NetworkRequest, + ApiResponseError, + ApiResponseSuccess } from '../../../internal/network'; import {UserApi, UserService, UserServiceImpl} from '../../../internal/user'; import {Environment, QonversionError, QonversionErrorCode, User} from '../../../index'; @@ -22,14 +23,14 @@ const apiPayload: UserApi = { id: testUserId, identity_id: 'some identity', }; -const testSuccessfulResponse: NetworkResponseSuccess = { +const testSuccessfulResponse: ApiResponseSuccess = { code: 200, data: apiPayload, isSuccess: true }; const testErrorCode = 500; const testErrorMessage = 'Test error message'; -const testErrorResponse: NetworkResponseError = { +const testErrorResponse: ApiResponseError = { code: testErrorCode, apiCode: '', message: testErrorMessage, @@ -96,7 +97,7 @@ describe('getUser tests', function () { test('user does not exist', async () => { // given - const testUserNotFoundResponse: NetworkResponseError = { + const testUserNotFoundResponse: ApiResponseError = { code: HTTP_CODE_NOT_FOUND, apiCode: '', message: testErrorMessage, diff --git a/sdk/src/__tests__/internal/userProperties/UserPropertiesController.test.ts b/sdk/src/__tests__/internal/userProperties/UserPropertiesController.test.ts index 9e8dba7..a572984 100644 --- a/sdk/src/__tests__/internal/userProperties/UserPropertiesController.test.ts +++ b/sdk/src/__tests__/internal/userProperties/UserPropertiesController.test.ts @@ -1,17 +1,18 @@ import { - UserPropertiesControllerImpl, + UserPropertiesControllerImpl, UserPropertiesSendResponse, UserPropertiesService, UserPropertiesStorage } from '../../../internal/userProperties'; import {DelayedWorker} from '../../../internal/utils/DelayedWorker'; import {Logger} from '../../../internal/logger'; -import {QonversionError, QonversionErrorCode, UserProperty} from '../../../index'; -import {UserChangedNotifier} from '../../../internal/user'; +import {QonversionError, QonversionErrorCode, UserPropertyKey} from '../../../index'; +import {UserChangedNotifier, UserDataStorage} from '../../../internal/user'; let userPropertiesController: UserPropertiesControllerImpl; let pendingUserPropertiesStorage: UserPropertiesStorage; let sentUserPropertiesStorage: UserPropertiesStorage; let userPropertiesService: UserPropertiesService; +let userDataStorage: UserDataStorage; let delayedWorker: DelayedWorker; let logger: Logger; let userChangedNotifier: UserChangedNotifier; @@ -25,6 +26,8 @@ beforeEach(() => { // @ts-ignore userPropertiesService = {}; // @ts-ignore + userDataStorage = {}; + // @ts-ignore delayedWorker = {}; // @ts-ignore logger = { @@ -38,6 +41,7 @@ beforeEach(() => { pendingUserPropertiesStorage, sentUserPropertiesStorage, userPropertiesService, + userDataStorage, delayedWorker, logger, userChangedNotifier, @@ -187,10 +191,15 @@ describe('sendUserPropertiesIfNeeded tests', () => { }); describe('sendUserProperties tests', () => { + const testUserId = 'Qon_test_user_id'; + beforeEach(() => { userPropertiesController['sendUserPropertiesIfNeeded'] = jest.fn(); pendingUserPropertiesStorage.delete = jest.fn(); sentUserPropertiesStorage.add = jest.fn(); + + userDataStorage.requireOriginalUserId = jest.fn(() => testUserId); + logger.warn = jest.fn(); logger.error = jest.fn(); }); @@ -199,21 +208,27 @@ describe('sendUserProperties tests', () => { // given const properties = {a: 'aa'}; pendingUserPropertiesStorage.getProperties = jest.fn(() => properties); - const processedPropertyKeys = Object.keys(properties); - userPropertiesService.sendProperties = jest.fn(async () => processedPropertyKeys); + const userPropertiesSendResponse: UserPropertiesSendResponse = { + propertyErrors: [], + savedProperties: [ + {key: 'a', value: 'aa'}, + ], + }; + userPropertiesService.sendProperties = jest.fn(async () => userPropertiesSendResponse); // when await userPropertiesController['sendUserProperties'](); // then expect(pendingUserPropertiesStorage.getProperties).toBeCalled(); - expect(userPropertiesService.sendProperties).toBeCalledWith(properties); + expect(userDataStorage.requireOriginalUserId).toBeCalled(); + expect(userPropertiesService.sendProperties).toBeCalledWith(testUserId, properties); expect(pendingUserPropertiesStorage.delete).toBeCalledWith(properties); expect(sentUserPropertiesStorage.add).toBeCalledWith(properties); expect(userPropertiesController['sendUserPropertiesIfNeeded']).toBeCalledWith(true); expect(logger.verbose).toBeCalledWith('Sending user properties', properties); - expect(logger.verbose).toBeCalledWith('User properties were sent', {processedPropertyKeys}); + expect(logger.verbose).toBeCalledWith('User properties were sent', userPropertiesSendResponse); expect(logger.warn).not.toBeCalled(); expect(logger.error).not.toBeCalled(); }); @@ -222,13 +237,18 @@ describe('sendUserProperties tests', () => { // given const properties = {}; pendingUserPropertiesStorage.getProperties = jest.fn(() => properties); - userPropertiesService.sendProperties = jest.fn(async () => []); + const userPropertiesSendResponse: UserPropertiesSendResponse = { + propertyErrors: [], + savedProperties: [], + }; + userPropertiesService.sendProperties = jest.fn(async () => userPropertiesSendResponse); // when await userPropertiesController['sendUserProperties'](); // then expect(pendingUserPropertiesStorage.getProperties).toBeCalled(); + expect(userDataStorage.requireOriginalUserId).not.toBeCalled(); expect(userPropertiesService.sendProperties).not.toBeCalled(); expect(pendingUserPropertiesStorage.delete).not.toBeCalled(); expect(sentUserPropertiesStorage.add).not.toBeCalled(); @@ -250,7 +270,8 @@ describe('sendUserProperties tests', () => { // then expect(pendingUserPropertiesStorage.getProperties).toBeCalled(); - expect(userPropertiesService.sendProperties).toBeCalledWith(properties); + expect(userDataStorage.requireOriginalUserId).toBeCalled(); + expect(userPropertiesService.sendProperties).toBeCalledWith(testUserId, properties); expect(logger.error).toBeCalledWith('Failed to send user properties to api', expError); expect(pendingUserPropertiesStorage.delete).not.toBeCalled(); expect(sentUserPropertiesStorage.add).not.toBeCalled(); @@ -262,23 +283,31 @@ describe('sendUserProperties tests', () => { // given const properties = {a: 'aa', b: 'bb'}; pendingUserPropertiesStorage.getProperties = jest.fn(() => properties); + const userPropertiesSendResponse: UserPropertiesSendResponse = { + propertyErrors: [ + {key: 'b', error: 'failed'}, + ], + savedProperties: [ + {key: 'a', value: 'aa'}, + ], + } + userPropertiesService.sendProperties = jest.fn(async () => userPropertiesSendResponse); + const processedProperties = {a: 'aa'}; - const processedPropertyKeys = Object.keys(processedProperties); - userPropertiesService.sendProperties = jest.fn(async () => processedPropertyKeys); // when await userPropertiesController['sendUserProperties'](); // then expect(pendingUserPropertiesStorage.getProperties).toBeCalled(); - expect(userPropertiesService.sendProperties).toBeCalledWith(properties); + expect(userDataStorage.requireOriginalUserId).toBeCalled(); + expect(userPropertiesService.sendProperties).toBeCalledWith(testUserId, properties); expect(pendingUserPropertiesStorage.delete).toBeCalledWith(properties); expect(sentUserPropertiesStorage.add).toBeCalledWith(processedProperties); - expect(logger.warn).toBeCalledWith('Some user properties were not processed: b.'); expect(userPropertiesController['sendUserPropertiesIfNeeded']).toBeCalledWith(true); expect(logger.verbose).toBeCalledWith('Sending user properties', properties); - expect(logger.verbose).toBeCalledWith('User properties were sent', {processedPropertyKeys}); + expect(logger.verbose).toBeCalledWith('User properties were sent', userPropertiesSendResponse); }); }); @@ -324,7 +353,7 @@ describe('Validator tests', () => { // given const testCases: Record = { test_key: true, - [UserProperty.AppsFlyerUserId]: true, + [UserPropertyKey.AppsFlyerUserId]: true, ['']: false, [' ']: false, ['test key']: false, @@ -468,4 +497,4 @@ describe('Validator tests', () => { expect(logger.error).toBeCalledWith(expKeyErrorMessage); expect(logger.error).toBeCalledWith(expValueErrorMessage); }); -}); \ No newline at end of file +}); diff --git a/sdk/src/__tests__/internal/userProperties/UserPropertiesService.test.ts b/sdk/src/__tests__/internal/userProperties/UserPropertiesService.test.ts index 13717be..d0a0d0d 100644 --- a/sdk/src/__tests__/internal/userProperties/UserPropertiesService.test.ts +++ b/sdk/src/__tests__/internal/userProperties/UserPropertiesService.test.ts @@ -1,59 +1,102 @@ import { + UserPropertiesSendResponse, UserPropertiesService, - UserPropertiesServiceImpl + UserPropertiesServiceImpl, UserPropertyData } from '../../../internal/userProperties'; import { ApiInteractor, RequestConfigurator, NetworkRequest, - NetworkResponseError, - NetworkResponseSuccess, + ApiResponseError, + ApiResponseSuccess, RequestConfiguratorImpl, RequestType } from '../../../internal/network'; import {QonversionError} from '../../../index'; -describe('UserPropertiesService tests', () => { - let userPropertiesService: UserPropertiesService; - let requestConfigurator: RequestConfigurator; - let apiInteractor: ApiInteractor; - let testRequest: NetworkRequest = { +let userPropertiesService: UserPropertiesService; +let requestConfigurator: RequestConfigurator; +let apiInteractor: ApiInteractor; +const testUserId = "Qon_test_user_id"; + +beforeEach(() => { + requestConfigurator = new (RequestConfiguratorImpl as any)(); + apiInteractor = { + execute: jest.fn(), + }; + userPropertiesService = new UserPropertiesServiceImpl(requestConfigurator, apiInteractor); +}); + +describe('Send properties tests', () => { + const testRequest: NetworkRequest = { body: {}, headers: {}, type: RequestType.POST, url: 'test url', }; - let testProperties = { + const testProperties = { a: 'a', b: 'b', }; + const testPropertiesData: UserPropertyData[] = [ + {key: 'a', value: 'a'}, + {key: 'b', value: 'b'}, + ]; beforeEach(() => { - requestConfigurator = new (RequestConfiguratorImpl as any)(); - requestConfigurator.configureUserPropertiesRequest = jest.fn(() => testRequest); - apiInteractor = { - execute: jest.fn(), - }; - userPropertiesService = new UserPropertiesServiceImpl(requestConfigurator, apiInteractor); + requestConfigurator.configureUserPropertiesSendRequest = jest.fn(() => testRequest); }); test('send properties success', async () => { // given - const expResult = ['a', 'b']; - const response: NetworkResponseSuccess<{data: {processed: string[]}}> = { + const expResult: UserPropertiesSendResponse = { + propertyErrors: [], + savedProperties: [ + {key: 'a', value: 'a'}, + {key: 'b', value: 'b'}, + ], + }; + const response: ApiResponseSuccess = { + code: 200, + data: expResult, + isSuccess: true, + }; + // @ts-ignore + apiInteractor.execute = jest.fn(async () => response); + + // when + const result = await userPropertiesService.sendProperties(testUserId, testProperties); + + // then + expect(result).toStrictEqual(expResult); + expect(requestConfigurator.configureUserPropertiesSendRequest).toBeCalledWith(testUserId, testPropertiesData); + expect(apiInteractor.execute).toBeCalledWith(testRequest); + }); + + test('send properties partial success', async () => { + // given + const expResult: UserPropertiesSendResponse = { + propertyErrors: [ + {key: 'a', error: 'failed'}, + ], + savedProperties: [ + {key: 'b', value: 'b'}, + ], + }; + const response: ApiResponseSuccess = { code: 200, - data: {data: {processed: expResult}}, + data: expResult, isSuccess: true, }; // @ts-ignore apiInteractor.execute = jest.fn(async () => response); // when - const result = await userPropertiesService.sendProperties(testProperties); + const result = await userPropertiesService.sendProperties(testUserId, testProperties); // then expect(result).toStrictEqual(expResult); - expect(requestConfigurator.configureUserPropertiesRequest).toBeCalledWith(testProperties); + expect(requestConfigurator.configureUserPropertiesSendRequest).toBeCalledWith(testUserId, testPropertiesData); expect(apiInteractor.execute).toBeCalledWith(testRequest); }); @@ -61,7 +104,64 @@ describe('UserPropertiesService tests', () => { // given const errorCode = 400; const errorMessage = 'test error message'; - const response: NetworkResponseError = { + const response: ApiResponseError = { + code: errorCode, + message: errorMessage, + apiCode: '', + type: '', + isSuccess: false + }; + apiInteractor.execute = jest.fn(async () => response); + + // when + await expect(async () => { + await userPropertiesService.sendProperties(testUserId, testProperties); + }).rejects.toThrow(QonversionError); + expect(requestConfigurator.configureUserPropertiesSendRequest).toBeCalledWith(testUserId, testPropertiesData); + expect(apiInteractor.execute).toBeCalledWith(testRequest); + }); +}); + +describe('Get properties tests', () => { + const testRequest: NetworkRequest = { + body: undefined, + headers: {}, + type: RequestType.GET, + url: 'test url', + }; + + beforeEach(() => { + requestConfigurator.configureUserPropertiesGetRequest = jest.fn(() => testRequest); + }); + + test('get properties success', async () => { + // given + const expResult: UserPropertyData[] = [ + {key: 'a', value: 'aa'}, + {key: 'b', value: 'bb'}, + ]; + const response: ApiResponseSuccess = { + code: 200, + data: expResult, + isSuccess: true, + }; + // @ts-ignore + apiInteractor.execute = jest.fn(async () => response); + + // when + const result = await userPropertiesService.getProperties(testUserId); + + // then + expect(result).toStrictEqual(expResult); + expect(requestConfigurator.configureUserPropertiesGetRequest).toBeCalledWith(testUserId); + expect(apiInteractor.execute).toBeCalledWith(testRequest); + }); + + test('get properties error', async () => { + // given + const errorCode = 400; + const errorMessage = 'test error message'; + const response: ApiResponseError = { code: errorCode, message: errorMessage, apiCode: '', @@ -72,9 +172,9 @@ describe('UserPropertiesService tests', () => { // when await expect(async () => { - await userPropertiesService.sendProperties(testProperties); + await userPropertiesService.getProperties(testUserId); }).rejects.toThrow(QonversionError); - expect(requestConfigurator.configureUserPropertiesRequest).toBeCalledWith(testProperties); + expect(requestConfigurator.configureUserPropertiesGetRequest).toBeCalledWith(testUserId); expect(apiInteractor.execute).toBeCalledWith(testRequest); }); }); diff --git a/sdk/src/dto/UserProperties.ts b/sdk/src/dto/UserProperties.ts new file mode 100644 index 0000000..464b01a --- /dev/null +++ b/sdk/src/dto/UserProperties.ts @@ -0,0 +1,76 @@ +import {UserProperty} from './UserProperty'; +import {UserPropertyKey} from './UserPropertyKey'; + +export class UserProperties { + /** + * List of all user properties. + */ + properties: UserProperty[]; + + /** + * List of user properties, set for the Qonversion defined keys. + * This is a subset of all {@link properties} list. + * See {@link QonversionInstance.setUserProperty}. + */ + definedProperties: UserProperty[]; + + /** + * List of user properties, set for custom keys. + * This is a subset of all {@link properties} list. + * See {@link QonversionInstance.setCustomUserProperty}. + */ + customProperties: UserProperty[]; + + /** + * Map of all user properties. + * This is a flattened version of the {@link properties} list as a key-value map. + */ + flatPropertiesMap: Map; + + /** + * Map of user properties, set for the Qonversion defined keys. + * This is a flattened version of the {@link definedProperties} list as a key-value map. + * See {@link QonversionInstance.setUserProperty}. + */ + flatDefinedPropertiesMap: Map; + + /** + * Map of user properties, set for custom keys. + * This is a flattened version of the {@link customProperties} list as a key-value map. + * See {@link QonversionInstance.setCustomUserProperty}. + */ + flatCustomPropertiesMap: Map; + + constructor(properties: UserProperty[]) { + this.properties = properties; + this.definedProperties = properties.filter(property => property.definedKey !== UserPropertyKey.Custom); + this.customProperties = properties.filter(property => property.definedKey === UserPropertyKey.Custom); + + this.flatPropertiesMap = new Map(); + this.flatDefinedPropertiesMap = new Map(); + this.flatCustomPropertiesMap = new Map(); + properties.forEach(property => { + this.flatPropertiesMap.set(property.key, property.value); + if (property.definedKey == UserPropertyKey.Custom) { + this.flatCustomPropertiesMap.set(property.key, property.value); + } else { + this.flatDefinedPropertiesMap.set(property.definedKey, property.value); + } + }); + } + + /** + * Searches for a property with the given property {@link key} in all properties list. + */ + getProperty(key: string): UserProperty | undefined { + return this.properties.find(userProperty => userProperty.key == key); + } + + /** + * Searches for a property with the given Qonversion defined property {@link key} + * in defined properties list. + */ + getDefinedProperty(key: UserPropertyKey): UserProperty | undefined { + return this.definedProperties.find(userProperty => userProperty.definedKey == key); + } +} diff --git a/sdk/src/dto/UserProperty.ts b/sdk/src/dto/UserProperty.ts index 5f0c2db..69e2c02 100644 --- a/sdk/src/dto/UserProperty.ts +++ b/sdk/src/dto/UserProperty.ts @@ -1,16 +1,19 @@ -/** - * This enum class represents all defined user property values - * that can be assigned to the user. Provide these keys along with values - * to {@link QonversionInstance.setUserProperty} method. - * See [the documentation](https://documentation.qonversion.io/docs/web-sdk#properties) for more information - */ -export enum UserProperty { - Email = "_q_email", - Name = "_q_name", - KochavaDeviceId = "_q_kochava_device_id", - AppsFlyerUserId = "_q_appsflyer_user_id", - AdjustAdId = "_q_adjust_adid", - CustomUserId = "_q_custom_user_id", - FacebookAttribution = "_q_fb_attribution", - FirebaseAppInstanceId = "_q_firebase_instance_id", +import {UserPropertyKey} from './UserPropertyKey'; +import {convertDefinedUserPropertyKey} from '../internal/utils/propertyUtils'; + +export class UserProperty { + key: string; + value: string; + + /** + * {@link UserPropertyKey} used to set this property. + * Returns {@link UserPropertyKey.Custom} for custom properties. + */ + definedKey: UserPropertyKey; + + constructor(key: string, value: string) { + this.key = key; + this.value = value; + this.definedKey = convertDefinedUserPropertyKey(key); + } } diff --git a/sdk/src/dto/UserPropertyKey.ts b/sdk/src/dto/UserPropertyKey.ts new file mode 100644 index 0000000..2c98776 --- /dev/null +++ b/sdk/src/dto/UserPropertyKey.ts @@ -0,0 +1,19 @@ +/** + * This enum class represents all defined user property keys + * that can be assigned to the user. Provide these keys along with values + * to {@link QonversionInstance.setUserProperty} method. + * See [the documentation](https://documentation.qonversion.io/docs/web-sdk#properties) for more information + */ +export enum UserPropertyKey { + Email = "_q_email", + Name = "_q_name", + KochavaDeviceId = "_q_kochava_device_id", + AppsFlyerUserId = "_q_appsflyer_user_id", + AdjustAdId = "_q_adjust_adid", + CustomUserId = "_q_custom_user_id", + FacebookAttribution = "_q_fb_attribution", + FirebaseAppInstanceId = "_q_firebase_instance_id", + AppSetId = "_q_app_set_id", + AdvertisingId = "_q_advertising_id", + Custom = "", +} diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 398104e..9cc132d 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -4,8 +4,10 @@ export * from './dto/Entitlement'; export * from './dto/Environment'; export * from './dto/LogLevel'; export * from './dto/Purchase'; -export * from './dto/UserProperty'; export * from './dto/User'; +export * from './dto/UserProperties'; +export * from './dto/UserProperty'; +export * from './dto/UserPropertyKey'; export * from './exception/QonversionError'; export * from './exception/QonversionErrorCode'; export * from './UserPropertiesBuilder'; diff --git a/sdk/src/internal/QonversionInternal.ts b/sdk/src/internal/QonversionInternal.ts index e27538e..ffb67f4 100644 --- a/sdk/src/internal/QonversionInternal.ts +++ b/sdk/src/internal/QonversionInternal.ts @@ -4,7 +4,7 @@ import {LogLevel} from '../dto/LogLevel'; import {Environment} from '../dto/Environment'; import {DependenciesAssembly} from './di/DependenciesAssembly'; import Qonversion from '../Qonversion'; -import {UserProperty} from '../dto/UserProperty'; +import {UserPropertyKey} from '../dto/UserPropertyKey'; import {UserPropertiesController} from './userProperties'; import {UserController} from './user'; import {EntitlementsController} from './entitlements'; @@ -12,6 +12,7 @@ import {Entitlement} from '../dto/Entitlement'; import {PurchasesController} from './purchases'; import {PurchaseCoreData, StripeStoreData, UserPurchase} from '../dto/Purchase'; import {Logger} from './logger'; +import {UserProperties} from '../dto/UserProperties'; export class QonversionInternal implements QonversionInstance { private readonly internalConfig: InternalConfig; @@ -24,11 +25,11 @@ export class QonversionInternal implements QonversionInstance { constructor(internalConfig: InternalConfig, dependenciesAssembly: DependenciesAssembly) { this.internalConfig = internalConfig; + this.logger = dependenciesAssembly.logger(); this.userPropertiesController = dependenciesAssembly.userPropertiesController(); this.userController = dependenciesAssembly.userController(); this.entitlementsController = dependenciesAssembly.entitlementsController(); this.purchasesController = dependenciesAssembly.purchasesController(); - this.logger = dependenciesAssembly.logger(); this.logger.verbose("The QonversionInstance is created"); } @@ -38,8 +39,8 @@ export class QonversionInternal implements QonversionInstance { return this.purchasesController.sendStripePurchase(data); } - getEntitlements(): Promise { - this.logger.verbose("getEntitlements() call"); + entitlements(): Promise { + this.logger.verbose("entitlements() call"); return this.entitlementsController.getEntitlements(); } @@ -63,11 +64,16 @@ export class QonversionInternal implements QonversionInstance { this.userPropertiesController.setProperties(userProperties); } - setUserProperty(property: UserProperty, value: string): void { + setUserProperty(property: UserPropertyKey, value: string): void { this.logger.verbose("setUserProperty() call"); this.userPropertiesController.setProperty(property, value); } + async userProperties(): Promise { + this.logger.verbose("userProperties() call"); + return await this.userPropertiesController.getProperties(); + } + finish() { this.logger.verbose("finish() call"); diff --git a/sdk/src/internal/di/ControllersAssembly.ts b/sdk/src/internal/di/ControllersAssembly.ts index 3f33415..6e71abe 100644 --- a/sdk/src/internal/di/ControllersAssembly.ts +++ b/sdk/src/internal/di/ControllersAssembly.ts @@ -22,6 +22,7 @@ export class ControllersAssemblyImpl implements ControllersAssembly { this.storageAssembly.pendingUserPropertiesStorage(), this.storageAssembly.sentUserPropertiesStorage(), this.servicesAssembly.userPropertiesService(), + this.storageAssembly.userDataStorage(), this.miscAssembly.delayedWorker(), this.miscAssembly.logger(), this.userController(), diff --git a/sdk/src/internal/entitlements/types.ts b/sdk/src/internal/entitlements/types.ts index 4cd272d..e355157 100644 --- a/sdk/src/internal/entitlements/types.ts +++ b/sdk/src/internal/entitlements/types.ts @@ -18,6 +18,7 @@ export type EntitlementApi = { active: boolean; started: number; expires: number; + source: string; product?: ProductApi; } diff --git a/sdk/src/internal/network/ApiInteractor.ts b/sdk/src/internal/network/ApiInteractor.ts index 48d9df5..4a7e9ad 100644 --- a/sdk/src/internal/network/ApiInteractor.ts +++ b/sdk/src/internal/network/ApiInteractor.ts @@ -3,8 +3,8 @@ import { ApiInteractor, NetworkClient, NetworkRequest, - NetworkResponseError, - NetworkResponseSuccess, + ApiResponseError, + ApiResponseSuccess, NetworkRetryConfig, RawNetworkResponse } from './types'; @@ -38,7 +38,7 @@ export class ApiInteractorImpl implements ApiInteractor { request: NetworkRequest, retryPolicy: RetryPolicy = this.defaultRetryPolicy, attemptIndex: number = 0, - ): Promise | NetworkResponseError> { + ): Promise | ApiResponseError> { if (!this.configHolder.canSendRequests()) { throw new QonversionError(QonversionErrorCode.RequestDenied); } @@ -81,7 +81,7 @@ export class ApiInteractorImpl implements ApiInteractor { return ApiInteractorImpl.getErrorResponse(response, executionError); } - static getErrorResponse(response?: RawNetworkResponse, executionError?: Error): NetworkResponseError { + static getErrorResponse(response?: RawNetworkResponse, executionError?: Error): ApiResponseError { if (response) { const apiError: ApiError = response.payload.error; return { diff --git a/sdk/src/internal/network/RequestConfigurator.ts b/sdk/src/internal/network/RequestConfigurator.ts index 9046daf..9831fe7 100644 --- a/sdk/src/internal/network/RequestConfigurator.ts +++ b/sdk/src/internal/network/RequestConfigurator.ts @@ -11,6 +11,7 @@ import {PrimaryConfigProvider} from '../types'; import {UserDataProvider} from '../user'; import {PurchaseCoreData, StripeStoreData} from '../../dto/Purchase'; import {Environment} from '../../dto/Environment'; +import {UserPropertyData} from '../userProperties'; export class RequestConfiguratorImpl implements RequestConfigurator { private readonly headerBuilder: HeaderBuilder; @@ -43,15 +44,14 @@ export class RequestConfiguratorImpl implements RequestConfigurator { return this.configureRequest(url, RequestType.POST, body); } - configureUserPropertiesRequest(properties: Record): NetworkRequest { - const url = `${this.baseUrl}/${ApiEndpoint.Properties}`; - const body = { - "access_token": this.primaryConfigProvider.getPrimaryConfig().projectKey, - "q_uid": this.userDataProvider.getOriginalUserId(), - "properties": properties - }; + configureUserPropertiesSendRequest(userId: string, properties: UserPropertyData[]): NetworkRequest { + const url = `${this.baseUrl}/${ApiEndpoint.Users}/${userId}/${ApiEndpoint.Properties}`; + return this.configureRequest(url, RequestType.POST, properties); + } - return this.configureRequest(url, RequestType.POST, body); + configureUserPropertiesGetRequest(userId: string): NetworkRequest { + const url = `${this.baseUrl}/${ApiEndpoint.Users}/${userId}/${ApiEndpoint.Properties}`; + return this.configureRequest(url, RequestType.GET); } configureCreateIdentityRequest(qonversionId: string, identityId: string): NetworkRequest { diff --git a/sdk/src/internal/network/types.ts b/sdk/src/internal/network/types.ts index f5156c7..00629a2 100644 --- a/sdk/src/internal/network/types.ts +++ b/sdk/src/internal/network/types.ts @@ -1,6 +1,7 @@ import {RetryPolicy} from './RetryPolicy'; import {PurchaseCoreData, StripeStoreData} from '../../dto/Purchase'; import {Environment} from '../../dto/Environment'; +import {UserPropertyData} from '../userProperties'; export enum ApiHeader { Accept = "Accept", @@ -17,7 +18,7 @@ export enum ApiHeader { export enum ApiEndpoint { Users = "v3/users", Identity = "v3/identities", - Properties = "v1/properties", + Properties = "properties", } export enum RequestType { @@ -28,7 +29,7 @@ export enum RequestType { } export type RequestHeaders = Record; -export type RequestBody = Record; +export type RequestBody = Record | Array; export type NetworkRequest = { url: string; @@ -37,23 +38,23 @@ export type NetworkRequest = { body?: RequestBody; }; -export type NetworkResponse = { +export type NetworkResponseBase = { code: number; }; -export type RawNetworkResponse = NetworkResponse & { +export type RawNetworkResponse = NetworkResponseBase & { // eslint-disable-next-line payload: any; }; -export type NetworkResponseError = NetworkResponse & { +export type ApiResponseError = NetworkResponseBase & { message: string; type?: string; apiCode?: string; isSuccess: false; }; -export type NetworkResponseSuccess = NetworkResponse & { +export type ApiResponseSuccess = NetworkResponseBase & { data: T; isSuccess: true; }; @@ -75,7 +76,7 @@ export type NetworkClient = { }; export type ApiInteractor = { - execute: (request: NetworkRequest, retryPolicy?: RetryPolicy) => Promise | NetworkResponseError>; + execute: (request: NetworkRequest, retryPolicy?: RetryPolicy) => Promise | ApiResponseError>; }; export type RequestConfigurator = { @@ -83,7 +84,9 @@ export type RequestConfigurator = { configureCreateUserRequest: (id: string, environment: Environment) => NetworkRequest; - configureUserPropertiesRequest: (properties: Record) => NetworkRequest; + configureUserPropertiesSendRequest: (userId: string, properties: UserPropertyData[]) => NetworkRequest; + + configureUserPropertiesGetRequest: (userId: string) => NetworkRequest; configureIdentityRequest: (identityId: string) => NetworkRequest; diff --git a/sdk/src/internal/purchases/types.ts b/sdk/src/internal/purchases/types.ts index 3995db5..8ee72b5 100644 --- a/sdk/src/internal/purchases/types.ts +++ b/sdk/src/internal/purchases/types.ts @@ -12,6 +12,7 @@ export type PurchaseCoreDataApi = { price: string; currency: string; purchased: number; + userId: string; }; export type StripeStoreDataApi = { diff --git a/sdk/src/internal/user/UserController.ts b/sdk/src/internal/user/UserController.ts index 073b42e..cc246dc 100644 --- a/sdk/src/internal/user/UserController.ts +++ b/sdk/src/internal/user/UserController.ts @@ -34,6 +34,7 @@ export class UserControllerImpl implements UserController { const existingUserId = userDataStorage.getOriginalUserId(); if (!existingUserId) { + this.logger.verbose('User doesn\'t exist, creating new one...'); this.createUser() .then(() => this.logger.info('New user created on initialization')) .catch(error => this.logger.error('Failed to create new user on initialization', error)); diff --git a/sdk/src/internal/userProperties/UserPropertiesController.ts b/sdk/src/internal/userProperties/UserPropertiesController.ts index 6cc824a..2694382 100644 --- a/sdk/src/internal/userProperties/UserPropertiesController.ts +++ b/sdk/src/internal/userProperties/UserPropertiesController.ts @@ -3,12 +3,16 @@ import {DelayedWorker} from '../utils/DelayedWorker'; import {Logger} from '../logger'; import {KEY_REGEX, SENDING_DELAY_MS} from './constants'; import {QonversionError} from '../../exception/QonversionError'; -import {UserChangedListener, UserChangedNotifier} from '../user'; +import {UserChangedListener, UserChangedNotifier, UserDataStorage} from '../user'; +import {UserProperties} from '../../dto/UserProperties'; +import {UserProperty} from '../../dto/UserProperty'; +import {UserPropertyKey} from '../../dto/UserPropertyKey'; export class UserPropertiesControllerImpl implements UserPropertiesController, UserChangedListener { private readonly pendingUserPropertiesStorage: UserPropertiesStorage; private readonly sentUserPropertiesStorage: UserPropertiesStorage; private readonly userPropertiesService: UserPropertiesService; + private readonly userDataStorage: UserDataStorage; private readonly delayedWorker: DelayedWorker; private readonly logger: Logger; private readonly sendingDelayMs: number; @@ -17,6 +21,7 @@ export class UserPropertiesControllerImpl implements UserPropertiesController, U pendingUserPropertiesStorage: UserPropertiesStorage, sentUserPropertiesStorage: UserPropertiesStorage, userPropertiesService: UserPropertiesService, + userDataStorage: UserDataStorage, delayedWorker: DelayedWorker, logger: Logger, userChangedNotifier: UserChangedNotifier, @@ -25,6 +30,7 @@ export class UserPropertiesControllerImpl implements UserPropertiesController, U this.pendingUserPropertiesStorage = pendingUserPropertiesStorage; this.sentUserPropertiesStorage = sentUserPropertiesStorage; this.userPropertiesService = userPropertiesService; + this.userDataStorage = userDataStorage; this.delayedWorker = delayedWorker; this.logger = logger; this.sendingDelayMs = sendingDelayMs; @@ -48,6 +54,20 @@ export class UserPropertiesControllerImpl implements UserPropertiesController, U this.sendUserPropertiesIfNeeded(); } + async getProperties(): Promise { + this.logger.verbose('Requesting user properties'); + + const userId = this.userDataStorage.requireOriginalUserId(); + const properties = await this.userPropertiesService.getProperties(userId); + + this.logger.verbose('User properties were received', properties); + + const mappedProperties = properties.map(userPropertyData => + new UserProperty(userPropertyData.key, userPropertyData.value) + ); + return new UserProperties(mappedProperties); + } + onUserChanged(): void { this.pendingUserPropertiesStorage.clear(); this.sentUserPropertiesStorage.clear(); @@ -70,34 +90,28 @@ export class UserPropertiesControllerImpl implements UserPropertiesController, U try { const propertiesToSend = {...this.pendingUserPropertiesStorage.getProperties()}; if (Object.keys(propertiesToSend).length === 0) { - return + return; } this.logger.verbose('Sending user properties', propertiesToSend); - const processedPropertyKeys = await this.userPropertiesService.sendProperties(propertiesToSend); - const nonProcessedProperties: Record = {}; + const userId = this.userDataStorage.requireOriginalUserId(); + const response = await this.userPropertiesService.sendProperties( + userId, + propertiesToSend, + ); + const processedProperties: Record = {}; - Object.keys(propertiesToSend).forEach(key => { - if (processedPropertyKeys.includes(key)) { - processedProperties[key] = propertiesToSend[key]; - } else { - nonProcessedProperties[key] = propertiesToSend[key]; - } + response.savedProperties.forEach(savedProperty => { + processedProperties[savedProperty.key] = savedProperty.value; }); - this.logger.verbose('User properties were sent', {processedPropertyKeys}); + this.logger.verbose('User properties were sent', response); // We delete all sent properties even if they were not successfully handled // to prevent spamming api with unacceptable properties. this.pendingUserPropertiesStorage.delete(propertiesToSend); this.sentUserPropertiesStorage.add(processedProperties); - const nonProcessedPropertyKeys = Object.keys(nonProcessedProperties); - if (nonProcessedPropertyKeys.length > 0) { - const joinedKeys = nonProcessedPropertyKeys.join(', '); - this.logger.warn(`Some user properties were not processed: ${joinedKeys}.`); - } - this.sendUserPropertiesIfNeeded(true); } catch (e) { if (e instanceof QonversionError) { @@ -108,7 +122,15 @@ export class UserPropertiesControllerImpl implements UserPropertiesController, U private shouldSendProperty(key: string, value: string): boolean { let shouldSend = true; - if (!UserPropertiesControllerImpl.isValidKey(key)) { + if (key == UserPropertyKey.Custom) { + shouldSend = false; + this.logger.warn( + "Can not set user property with the key `UserPropertyKey.Custom`. " + + "To set custom user property, use the `setCustomUserProperty` method." + ); + } + + if (shouldSend && !UserPropertiesControllerImpl.isValidKey(key)) { shouldSend = false; this.logger.error( `Invalid key "${key}" for user property. ` + diff --git a/sdk/src/internal/userProperties/UserPropertiesService.ts b/sdk/src/internal/userProperties/UserPropertiesService.ts index 9ac27da..422892c 100644 --- a/sdk/src/internal/userProperties/UserPropertiesService.ts +++ b/sdk/src/internal/userProperties/UserPropertiesService.ts @@ -1,4 +1,4 @@ -import {UserPropertiesService} from './types'; +import {UserPropertiesSendResponse, UserPropertiesService, UserPropertyData} from './types'; import {ApiInteractor, RequestConfigurator} from '../network'; import {QonversionError} from '../../exception/QonversionError'; import {QonversionErrorCode} from '../../exception/QonversionErrorCode'; @@ -12,12 +12,29 @@ export class UserPropertiesServiceImpl implements UserPropertiesService { this.apiInteractor = apiInteractor; } - async sendProperties(properties: Record): Promise { - const request = this.requestConfigurator.configureUserPropertiesRequest(properties); - const response = await this.apiInteractor.execute<{data: {processed: string[]}}>(request); + async sendProperties(userId: string, properties: Record): Promise { + const propertiesList: UserPropertyData[] = Object.keys(properties).map(key => ({ + key, + value: properties[key], + })); + + const request = this.requestConfigurator.configureUserPropertiesSendRequest(userId, propertiesList); + const response = await this.apiInteractor.execute(request); + + if (response.isSuccess) { + return response.data; + } + + const errorMessage = `Response code ${response.code}, message: ${response.message}`; + throw new QonversionError(QonversionErrorCode.BackendError, errorMessage); + } + + async getProperties(userId: string): Promise { + const request = this.requestConfigurator.configureUserPropertiesGetRequest(userId); + const response = await this.apiInteractor.execute(request); if (response.isSuccess) { - return response.data.data.processed; + return response.data; } const errorMessage = `Response code ${response.code}, message: ${response.message}`; diff --git a/sdk/src/internal/userProperties/types.ts b/sdk/src/internal/userProperties/types.ts index 138dfb3..5990a80 100644 --- a/sdk/src/internal/userProperties/types.ts +++ b/sdk/src/internal/userProperties/types.ts @@ -1,3 +1,5 @@ +import {UserProperties} from '../../dto/UserProperties'; + export type UserPropertiesStorage = { getProperties: () => Record; @@ -13,11 +15,27 @@ export type UserPropertiesStorage = { }; export type UserPropertiesService = { - sendProperties: (properties: Record) => Promise; + sendProperties: (userId: string, properties: Record) => Promise; + getProperties: (userId: string) => Promise; }; export type UserPropertiesController = { setProperty: (key: string, value: string) => void; - setProperties: (properties: Record) => void; + getProperties: () => Promise; +}; + +export type UserPropertyData = { + key: string; + value: string; +}; + +export type UserPropertyError = { + key: string; + error: string; +}; + +export type UserPropertiesSendResponse = { + savedProperties: UserPropertyData[], + propertyErrors: UserPropertyError[], }; diff --git a/sdk/src/internal/utils/propertyUtils.ts b/sdk/src/internal/utils/propertyUtils.ts new file mode 100644 index 0000000..2abc683 --- /dev/null +++ b/sdk/src/internal/utils/propertyUtils.ts @@ -0,0 +1,5 @@ +import {UserPropertyKey} from '../../dto/UserPropertyKey'; + +export const convertDefinedUserPropertyKey = (sourceKey: string): UserPropertyKey => { + return Object.values(UserPropertyKey).find(userPropertyKey => userPropertyKey == sourceKey) ?? UserPropertyKey.Custom; +}; diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 9a89056..8f161a9 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -1,8 +1,9 @@ import {LogLevel} from './dto/LogLevel'; import {Environment} from './dto/Environment'; -import {UserProperty} from './dto/UserProperty'; +import {UserPropertyKey} from './dto/UserPropertyKey'; import {Entitlement} from './dto/Entitlement'; import {PurchaseCoreData, StripeStoreData, UserPurchase} from './dto/Purchase'; +import {UserProperties} from './dto/UserProperties'; export type QonversionInstance = { /** @@ -21,12 +22,12 @@ export type QonversionInstance = { * * @returns a list of current user entitlements. */ - getEntitlements: () => Promise; + entitlements: () => Promise; /** * Call this function to link a user to his unique id in your system and share purchase data. * If you want to check identified user permissions await for returned promise to resolve and - * call {@link getEntitlements} then. + * call {@link entitlements} then. * * @param userId - unique user id in your system */ @@ -35,7 +36,7 @@ export type QonversionInstance = { /** * Call this function to unlink a user from his unique ID in your system and his purchase data. * If you want to check logged-out user permissions await for returned promise to resolve and - * call {@link getEntitlements} then. + * call {@link entitlements} then. */ logout: () => Promise; @@ -46,12 +47,15 @@ export type QonversionInstance = { * This method consumes only defined user properties. In order to pass custom property * consider using {@link setCustomUserProperty} method. * + * Note that using {@link UserPropertyKey.Custom} here will do nothing. + * To set custom user property, use {@link setCustomUserProperty} method instead. + * * You can either pass multiple properties at once using {@link setUserProperties} method. * * @param property defined user attribute * @param value nonempty value for the given property */ - setUserProperty: (property: UserProperty, value: string) => void; + setUserProperty: (property: UserPropertyKey, value: string) => void; /** * Add property value for the current user to use it then for segmentation or analytics @@ -67,13 +71,22 @@ export type QonversionInstance = { */ setCustomUserProperty: (key: string, value: string) => void; + /** + * This method returns all the properties, set for the current Qonversion user. + * All set properties are sent to the server with delay, so if you call + * this function right after setting some property, it may not be included + * in the result. + * @returns the promise with the user properties + */ + userProperties(): Promise; + /** * Add a property value for the current user to use it then for segmentation or analytics * as well as to provide it to third-party platforms. * * This method consumes both defined and custom user properties. Consider using * {@link UserPropertiesBuilder} to prepare a properties map. You are also able to create it - * on your own using a custom key for a custom property or {@link UserProperty} code as the key for + * on your own using a custom key for a custom property or {@link UserPropertyKey} code as the key for * a Qonversion defined property. * * In order to pass a single property consider using {@link setCustomUserProperty} method for From 9674156bee9d829f13a0fa2f79d0031778013055 Mon Sep 17 00:00:00 2001 From: SpertsyanKM Date: Mon, 14 Aug 2023 10:48:28 +0000 Subject: [PATCH 3/3] [create-pull-request] automated change --- fastlane/report.xml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/report.xml b/fastlane/report.xml index 0c291f4..5a09b23 100644 --- a/fastlane/report.xml +++ b/fastlane/report.xml @@ -5,7 +5,7 @@ - + diff --git a/package.json b/package.json index 0a1d466..f4a00c2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@qonversion/web-sdk", "title": "Qonversion Web SDK", - "version": "0.2.2", + "version": "1.0.0", "description": "Qonversion provides full in-app purchases infrastructure, so you do not need to build your own server for receipt validation. Implement in-app subscriptions, validate user receipts, check subscription status, and provide access to your app features and content using our Stripe wrapper, StoreKit wrapper and Google Play Billing wrapper.", "main": "sdk/build/index.js", "types": "sdk/build/index.d.ts",