From 4b30b78adf886b0b7ff71dda0ce244122dcb8163 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 11 Apr 2025 15:51:04 -0700 Subject: [PATCH 1/6] add identity model logic --- .../executors/IdentityOperationExecutor.ts | 199 ++++++++++++++++++ src/core/modelRepo/IdentityModel.ts | 33 +++ src/core/modelRepo/ModelStore.ts | 6 +- src/shared/models/SimpleModelStore.ts | 34 +++ src/shared/models/SingletonModelStore.ts | 72 +++++++ src/types/backend.ts | 47 +++++ src/types/models.ts | 50 ++++- 7 files changed, 433 insertions(+), 8 deletions(-) create mode 100644 src/core/executors/IdentityOperationExecutor.ts create mode 100644 src/core/modelRepo/IdentityModel.ts create mode 100644 src/shared/models/SimpleModelStore.ts create mode 100644 src/shared/models/SingletonModelStore.ts create mode 100644 src/types/backend.ts diff --git a/src/core/executors/IdentityOperationExecutor.ts b/src/core/executors/IdentityOperationExecutor.ts new file mode 100644 index 000000000..ac7add4d7 --- /dev/null +++ b/src/core/executors/IdentityOperationExecutor.ts @@ -0,0 +1,199 @@ +import { IIdentityBackendService } from 'src/types/backend'; +import { IOperationExecutor } from 'src/types/operation'; + +export class IdentityOperationExecutor implements IOperationExecutor { + private readonly _identityBackend: IIdentityBackendService; + private readonly _identityModelStore: IdentityModelStore; + private readonly _buildUserService: IRebuildUserService; + private readonly _newRecordState: NewRecordsState; + + static readonly SET_ALIAS = 'set-alias'; + static readonly DELETE_ALIAS = 'delete-alias'; + + constructor( + identityBackend: IIdentityBackendService, + identityModelStore: IdentityModelStore, + buildUserService: IRebuildUserService, + newRecordState: NewRecordsState, + ) { + this._identityBackend = identityBackend; + this._identityModelStore = identityModelStore; + this._buildUserService = buildUserService; + this._newRecordState = newRecordState; + } + + get operations(): string[] { + return [ + IdentityOperationExecutor.SET_ALIAS, + IdentityOperationExecutor.DELETE_ALIAS, + ]; + } + + async execute(operations: Operation[]): Promise { + Logging.debug( + `IdentityOperationExecutor(operations: ${JSON.stringify(operations)})`, + ); + + const invalidOps = operations.filter( + (op) => + !( + op instanceof SetAliasOperation || op instanceof DeleteAliasOperation + ), + ); + if (invalidOps.length > 0) { + throw new Error( + `Unrecognized operation(s)! Attempted operations:\n${JSON.stringify( + operations, + )}`, + ); + } + + const hasSetAlias = operations.some( + (op) => op instanceof SetAliasOperation, + ); + const hasDeleteAlias = operations.some( + (op) => op instanceof DeleteAliasOperation, + ); + if (hasSetAlias && hasDeleteAlias) { + throw new Error( + `Can't process SetAliasOperation and DeleteAliasOperation at the same time.`, + ); + } + + const lastOperation = operations[operations.length - 1]; + + if (lastOperation instanceof SetAliasOperation) { + try { + await this._identityBackend.setAlias( + lastOperation.appId, + IdentityConstants.ONESIGNAL_ID, + lastOperation.onesignalId, + { [lastOperation.label]: lastOperation.value }, + ); + + if ( + this._identityModelStore.model.onesignalId === + lastOperation.onesignalId + ) { + this._identityModelStore.model.setStringProperty( + lastOperation.label, + lastOperation.value, + ModelChangeTags.HYDRATE, + ); + } + } catch (ex) { + if (ex instanceof BackendException) { + const responseType = NetworkUtils.getResponseStatusType( + ex.statusCode, + ); + + switch (responseType) { + case NetworkUtils.ResponseStatusType.RETRYABLE: + return new ExecutionResponse( + ExecutionResult.FAIL_RETRY, + ex.retryAfterSeconds, + ); + case NetworkUtils.ResponseStatusType.INVALID: + return new ExecutionResponse(ExecutionResult.FAIL_NORETRY); + case NetworkUtils.ResponseStatusType.CONFLICT: + return new ExecutionResponse( + ExecutionResult.FAIL_CONFLICT, + ex.retryAfterSeconds, + ); + case NetworkUtils.ResponseStatusType.UNAUTHORIZED: + return new ExecutionResponse( + ExecutionResult.FAIL_UNAUTHORIZED, + ex.retryAfterSeconds, + ); + case NetworkUtils.ResponseStatusType.MISSING: + if ( + ex.statusCode === 404 && + this._newRecordState.isInMissingRetryWindow( + lastOperation.onesignalId, + ) + ) { + return new ExecutionResponse( + ExecutionResult.FAIL_RETRY, + ex.retryAfterSeconds, + ); + } + + const rebuildOps = + this._buildUserService.getRebuildOperationsIfCurrentUser( + lastOperation.appId, + lastOperation.onesignalId, + ); + if (!rebuildOps) { + return new ExecutionResponse(ExecutionResult.FAIL_NORETRY); + } else { + return new ExecutionResponse( + ExecutionResult.FAIL_RETRY, + ex.retryAfterSeconds, + rebuildOps, + ); + } + } + } + } + } else if (lastOperation instanceof DeleteAliasOperation) { + try { + await this._identityBackend.deleteAlias( + lastOperation.appId, + IdentityConstants.ONESIGNAL_ID, + lastOperation.onesignalId, + lastOperation.label, + ); + + if ( + this._identityModelStore.model.onesignalId === + lastOperation.onesignalId + ) { + this._identityModelStore.model.setOptStringProperty( + lastOperation.label, + null, + ModelChangeTags.HYDRATE, + ); + } + } catch (ex) { + if (ex instanceof BackendException) { + const responseType = NetworkUtils.getResponseStatusType( + ex.statusCode, + ); + + switch (responseType) { + case NetworkUtils.ResponseStatusType.RETRYABLE: + return new ExecutionResponse( + ExecutionResult.FAIL_RETRY, + ex.retryAfterSeconds, + ); + case NetworkUtils.ResponseStatusType.CONFLICT: + return new ExecutionResponse(ExecutionResult.SUCCESS); // alias doesn’t exist = good + case NetworkUtils.ResponseStatusType.INVALID: + return new ExecutionResponse(ExecutionResult.FAIL_NORETRY); + case NetworkUtils.ResponseStatusType.UNAUTHORIZED: + return new ExecutionResponse( + ExecutionResult.FAIL_UNAUTHORIZED, + ex.retryAfterSeconds, + ); + case NetworkUtils.ResponseStatusType.MISSING: + if ( + ex.statusCode === 404 && + this._newRecordState.isInMissingRetryWindow( + lastOperation.onesignalId, + ) + ) { + return new ExecutionResponse( + ExecutionResult.FAIL_RETRY, + ex.retryAfterSeconds, + ); + } else { + return new ExecutionResponse(ExecutionResult.SUCCESS); // already deleted + } + } + } + } + } + + return new ExecutionResponse(ExecutionResult.SUCCESS); + } +} diff --git a/src/core/modelRepo/IdentityModel.ts b/src/core/modelRepo/IdentityModel.ts new file mode 100644 index 000000000..b14392f7c --- /dev/null +++ b/src/core/modelRepo/IdentityModel.ts @@ -0,0 +1,33 @@ +import { IdentityConstants } from 'src/types/backend'; +import { Model } from './Model'; + +/** + * The identity model as a MapModel: a simple key-value pair where the key represents + * the alias label and the value represents the alias ID for that alias label. + * This model provides simple access to more well-defined aliases. + */ +export class IdentityModel extends Model { + /** + * The OneSignal ID for this identity. + * WARNING: This *might* be a local ID depending on whether the user has been + * successfully created on the backend or not. + */ + get onesignalId(): string { + return this.getProperty(IdentityConstants.ONESIGNAL_ID); + } + + set onesignalId(value: string) { + this.setProperty(IdentityConstants.ONESIGNAL_ID, value); + } + + /** + * The (developer-managed) identifier that uniquely identifies this user. + */ + get externalId(): string | null { + return this.getProperty(IdentityConstants.EXTERNAL_ID); + } + + set externalId(value: string | null) { + this.setProperty(IdentityConstants.EXTERNAL_ID, value); + } +} diff --git a/src/core/modelRepo/ModelStore.ts b/src/core/modelRepo/ModelStore.ts index e7ba27254..5cfdbfc83 100644 --- a/src/core/modelRepo/ModelStore.ts +++ b/src/core/modelRepo/ModelStore.ts @@ -52,15 +52,15 @@ export abstract class ModelStore /** * Create a model from JSON data */ - abstract create(json: object): TModel | null; + abstract create(json?: object): TModel | null; - add(model: TModel, tag: string): void { + add(model: TModel, tag = ModelChangeTags.NORMAL): void { const oldModel = this.models.find((m) => m.id === model.id); if (oldModel) this.removeItem(oldModel, tag); this.addItem(model, tag); } - addAt(index: number, model: TModel, tag: string): void { + addAt(index: number, model: TModel, tag = ModelChangeTags.NORMAL): void { const oldModel = this.models.find((m) => m.id === model.id); if (oldModel) this.removeItem(oldModel, tag); this.addItem(model, tag, index); diff --git a/src/shared/models/SimpleModelStore.ts b/src/shared/models/SimpleModelStore.ts new file mode 100644 index 000000000..6114a7534 --- /dev/null +++ b/src/shared/models/SimpleModelStore.ts @@ -0,0 +1,34 @@ +import { Model } from 'src/core/modelRepo/Model'; +import { ModelStore } from 'src/core/modelRepo/ModelStore'; +import { IPreferencesService } from 'src/types/preferences'; + +/** + * A simple model store is a concrete implementation of the ModelStore, + * which provides a basic create() method using the passed-in factory. + */ +export class SimpleModelStore extends ModelStore { + private readonly _create: () => TModel; + + /** + * @param _create A factory function used to instantiate a new model instance. + * @param name Optional name for persistence. + * @param _prefs Optional preferences service for persistence support. + */ + constructor( + _create: () => TModel, + name?: string, + _prefs?: IPreferencesService, + ) { + super(name, _prefs); + this._create = _create; + this.load(); // Automatically load on construction + } + + override create(jsonObject?: object): TModel { + const model = this._create(); + if (jsonObject != null) { + model.initializeFromJson(jsonObject); + } + return model; + } +} diff --git a/src/shared/models/SingletonModelStore.ts b/src/shared/models/SingletonModelStore.ts new file mode 100644 index 000000000..f3a1fa3bb --- /dev/null +++ b/src/shared/models/SingletonModelStore.ts @@ -0,0 +1,72 @@ +import { Model, ModelChangedArgs } from 'src/core/modelRepo/Model'; +import { ModelStore } from 'src/core/modelRepo/ModelStore'; +import { + IModelStoreChangeHandler, + ISingletonModelStore, + ISingletonModelStoreChangeHandler, +} from 'src/types/models'; +import { EventProducer } from '../helpers/EventProducer'; + +export class SingletonModelStore + implements ISingletonModelStore, IModelStoreChangeHandler +{ + private readonly store: ModelStore; + private readonly changeSubscription = new EventProducer< + ISingletonModelStoreChangeHandler + >(); + private readonly singletonId = '-singleton-'; + + constructor(store: ModelStore) { + this.store = store; + store.subscribe(this); + } + + get model(): TModel { + const model = this.store.get(this.singletonId); + if (model) return model; + + const createdModel = this.store.create(); + if (!createdModel) + throw new Error(`Unable to initialize model from store ${this.store}`); + + createdModel.id = this.singletonId; + this.store.add(createdModel); + return createdModel; + } + + replace(model: TModel, tag: string): void { + const existingModel = this.model; + existingModel.initializeFromModel(this.singletonId, model); + this.store.persist(); + this.changeSubscription.fire((handler) => + handler.onModelReplaced(existingModel, tag), + ); + } + + subscribe(handler: ISingletonModelStoreChangeHandler): void { + this.changeSubscription.subscribe(handler); + } + + unsubscribe(handler: ISingletonModelStoreChangeHandler): void { + this.changeSubscription.unsubscribe(handler); + } + + get hasSubscribers(): boolean { + return this.changeSubscription.hasSubscribers; + } + + // These are no-ops by design + onModelAdded(model: TModel, tag: string): void { + // No-op: singleton is transparently added + } + + onModelUpdated(args: ModelChangedArgs, tag: string): void { + this.changeSubscription.fire((handler) => + handler.onModelUpdated(args, tag), + ); + } + + onModelRemoved(model: TModel, tag: string): void { + // No-op: singleton is never removed + } +} diff --git a/src/types/backend.ts b/src/types/backend.ts new file mode 100644 index 000000000..b4a7188da --- /dev/null +++ b/src/types/backend.ts @@ -0,0 +1,47 @@ +export interface IIdentityBackendService { + /** + * Set one or more aliases for the user identified by the aliasLabel/aliasValue provided. + * If there is a non-successful response from the backend, a BackendException should be thrown with response data. + * + * @param appId - The ID of the OneSignal application this user exists under. + * @param aliasLabel - The alias label to retrieve the user under. + * @param aliasValue - The identifier within the aliasLabel that identifies the user to retrieve. + * @param identities - The identities that are to be created. + * @throws BackendException + */ + setAlias( + appId: string, + aliasLabel: string, + aliasValue: string, + identities: Record, + ): Promise>; + + /** + * Delete the aliasLabelToDelete from the user identified by the aliasLabel/aliasValue provided. + * If there is a non-successful response from the backend, a BackendException should be thrown with response data. + * + * @param appId - The ID of the OneSignal application this user exists under. + * @param aliasLabel - The alias label to retrieve the user under. + * @param aliasValue - The identifier within the aliasLabel that identifies the user to retrieve. + * @param aliasLabelToDelete - The alias label to delete from the user identified. + * @throws BackendException + */ + deleteAlias( + appId: string, + aliasLabel: string, + aliasValue: string, + aliasLabelToDelete: string, + ): Promise; +} + +export const IdentityConstants = { + /** + * The alias label for the external ID alias. + */ + EXTERNAL_ID: 'external_id', + + /** + * The alias label for the internal OneSignal ID alias. + */ + ONESIGNAL_ID: 'onesignal_id', +} as const; diff --git a/src/types/models.ts b/src/types/models.ts index 02f381d75..595a57497 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -1,6 +1,12 @@ import type { Model, ModelChangedArgs } from 'src/core/models/Model'; import type { IEventNotifier } from './events'; +export const ModelChangeTags = { + NORMAL: 'NORMAL', + NO_PROPOGATE: 'NO_PROPOGATE', + HYDRATE: 'HYDRATE', +} as const; + /** * A handler interface for subscribing to model change events for a specific model store. */ @@ -69,8 +75,42 @@ export interface IModelStore replaceAll(models: TModel[], tag?: string): void; } -export const ModelChangeTags = { - NORMAL: 'NORMAL', - NO_PROPOGATE: 'NO_PROPOGATE', - HYDRATE: 'HYDRATE', -} as const; +// SINGLETON MODEL STORE +/** + * A handler interface for ISingletonModelStore.subscribe. + * Implement this interface to subscribe to model change events for a specific model store. + */ +export interface ISingletonModelStoreChangeHandler { + /** + * Called when the model has been replaced. + * + * @param model - The new model. + * @param tag - The tag which identifies how/why the model was replaced. + */ + onModelReplaced(model: TModel, tag: string): void; + + /** + * Called when a property within the model has been updated. + * This wraps IModelChangedHandler.onChanged so store users don't need to subscribe directly to the model. + * + * @param args - The model change arguments. + * @param tag - The tag which identifies how/why the model was updated. + */ + onModelUpdated(args: ModelChangedArgs, tag: string): void; +} + +export interface ISingletonModelStore + extends IEventNotifier> { + /** + * The model managed by this singleton model store. + */ + readonly model: TModel; + + /** + * Replace the existing model with the new model provided. + * + * @param model - A model that contains all the data for the new effective model. + * @param tag - A tag identifying how/why the model is being replaced. + */ + replace(model: TModel, tag?: string): void; +} From 43a8a69170dcd509c136d7dbf356a166f97f06bc Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 11 Apr 2025 16:25:49 -0700 Subject: [PATCH 2/6] add IdentityModelStore --- __test__/support/helpers/core.ts | 10 ++--- __test__/support/helpers/pushSubscription.ts | 2 +- __test__/unit/core/modelCache.test.ts | 2 +- __test__/unit/core/osModel.test.ts | 4 +- src/core/modelRepo/IdentityModel.ts | 33 ---------------- src/core/modelRepo/OSModelStore.ts | 2 +- src/core/models/IdentityModel.ts | 39 +++++++++++++++---- src/core/models/IdentityModelStore.ts | 13 +++++++ src/onesignal/PushSubscriptionNamespace.ts | 12 +++--- src/onesignal/User.ts | 4 +- src/shared/managers/SubscriptionManager.ts | 40 ++++++++++---------- 11 files changed, 82 insertions(+), 79 deletions(-) delete mode 100644 src/core/modelRepo/IdentityModel.ts create mode 100644 src/core/models/IdentityModelStore.ts diff --git a/__test__/support/helpers/core.ts b/__test__/support/helpers/core.ts index f0830f5b0..0960308aa 100644 --- a/__test__/support/helpers/core.ts +++ b/__test__/support/helpers/core.ts @@ -1,20 +1,20 @@ -import { SupportedIdentity } from '../../../src/core/models/IdentityModel'; +import CoreModule from '../../../src/core/CoreModule'; +import { CoreModuleDirector } from '../../../src/core/CoreModuleDirector'; import { OSModel } from '../../../src/core/modelRepo/OSModel'; import { CoreChangeType } from '../../../src/core/models/CoreChangeType'; import { CoreDelta } from '../../../src/core/models/CoreDeltas'; +import { SupportedIdentity } from '../../../src/core/models/IdentityModel'; import { - SupportedSubscription, SubscriptionType, + SupportedSubscription, } from '../../../src/core/models/SubscriptionModels'; import { ModelName } from '../../../src/core/models/SupportedModels'; +import { UserPropertiesModel } from '../../../src/core/models/UserPropertiesModel'; import { DUMMY_MODEL_ID, DUMMY_PUSH_TOKEN, DUMMY_SUBSCRIPTION_ID, } from '../constants'; -import CoreModule from '../../../src/core/CoreModule'; -import { CoreModuleDirector } from '../../../src/core/CoreModuleDirector'; -import { UserPropertiesModel } from '../../../src/core/models/UserPropertiesModel'; export function generateNewSubscription(modelId = '0000000000') { return new OSModel( diff --git a/__test__/support/helpers/pushSubscription.ts b/__test__/support/helpers/pushSubscription.ts index 7a71e853a..58bb65b79 100644 --- a/__test__/support/helpers/pushSubscription.ts +++ b/__test__/support/helpers/pushSubscription.ts @@ -3,8 +3,8 @@ import { ModelName, SupportedModel, } from '../../../src/core/models/SupportedModels'; -import { getDummyPushSubscriptionOSModel } from './core'; import { TestEnvironment } from '../environment/TestEnvironment'; +import { getDummyPushSubscriptionOSModel } from './core'; export async function initializeWithPermission( permission: NotificationPermission, diff --git a/__test__/unit/core/modelCache.test.ts b/__test__/unit/core/modelCache.test.ts index f92e673e7..788e9dc7c 100644 --- a/__test__/unit/core/modelCache.test.ts +++ b/__test__/unit/core/modelCache.test.ts @@ -1,7 +1,7 @@ +import ModelCache from '../../../src/core/caching/ModelCache'; import { IdentityExecutor } from '../../../src/core/executors/IdentityExecutor'; import { PropertiesExecutor } from '../../../src/core/executors/PropertiesExecutor'; import { SubscriptionExecutor } from '../../../src/core/executors/SubscriptionExecutor'; -import ModelCache from '../../../src/core/caching/ModelCache'; import { OSModel } from '../../../src/core/modelRepo/OSModel'; import { ModelName } from '../../../src/core/models/SupportedModels'; import { TestEnvironment } from '../../support/environment/TestEnvironment'; diff --git a/__test__/unit/core/osModel.test.ts b/__test__/unit/core/osModel.test.ts index 8f76cb688..e2e66a590 100644 --- a/__test__/unit/core/osModel.test.ts +++ b/__test__/unit/core/osModel.test.ts @@ -1,10 +1,10 @@ -import { ModelName } from '../../../src/core/models/SupportedModels'; +import { OSModel } from '../../../src/core/modelRepo/OSModel'; import { SubscriptionModel, SubscriptionType, } from '../../../src/core/models/SubscriptionModels'; +import { ModelName } from '../../../src/core/models/SupportedModels'; import { generateNewSubscription } from '../../support/helpers/core'; -import { OSModel } from '../../../src/core/modelRepo/OSModel'; describe('OSModel tests', () => { test('Set function updates data', async () => { diff --git a/src/core/modelRepo/IdentityModel.ts b/src/core/modelRepo/IdentityModel.ts deleted file mode 100644 index b14392f7c..000000000 --- a/src/core/modelRepo/IdentityModel.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { IdentityConstants } from 'src/types/backend'; -import { Model } from './Model'; - -/** - * The identity model as a MapModel: a simple key-value pair where the key represents - * the alias label and the value represents the alias ID for that alias label. - * This model provides simple access to more well-defined aliases. - */ -export class IdentityModel extends Model { - /** - * The OneSignal ID for this identity. - * WARNING: This *might* be a local ID depending on whether the user has been - * successfully created on the backend or not. - */ - get onesignalId(): string { - return this.getProperty(IdentityConstants.ONESIGNAL_ID); - } - - set onesignalId(value: string) { - this.setProperty(IdentityConstants.ONESIGNAL_ID, value); - } - - /** - * The (developer-managed) identifier that uniquely identifies this user. - */ - get externalId(): string | null { - return this.getProperty(IdentityConstants.EXTERNAL_ID); - } - - set externalId(value: string | null) { - this.setProperty(IdentityConstants.EXTERNAL_ID, value); - } -} diff --git a/src/core/modelRepo/OSModelStore.ts b/src/core/modelRepo/OSModelStore.ts index 6033d3435..476bf44c9 100644 --- a/src/core/modelRepo/OSModelStore.ts +++ b/src/core/modelRepo/OSModelStore.ts @@ -1,4 +1,3 @@ -import Subscribable from '../Subscribable'; import { CoreChangeType } from '../models/CoreChangeType'; import { ModelStoreAdded, @@ -7,6 +6,7 @@ import { ModelStoreRemoved, ModelStoreUpdated, } from '../models/ModelStoreChange'; +import Subscribable from '../Subscribable'; import { isOSModel, isOSModelUpdatedArgs } from '../utils/typePredicates'; import { OSModel } from './OSModel'; diff --git a/src/core/models/IdentityModel.ts b/src/core/models/IdentityModel.ts index b88818fb1..629ef1ddc 100644 --- a/src/core/models/IdentityModel.ts +++ b/src/core/models/IdentityModel.ts @@ -1,10 +1,33 @@ -export interface IdentityModel extends FutureIdentityModel { - onesignal_id: string; -} +import { IdentityConstants } from 'src/types/backend'; +import { Model } from '../modelRepo/Model'; -export interface FutureIdentityModel { - external_id?: string; - [key: string]: string | undefined; -} +/** + * The identity model as a MapModel: a simple key-value pair where the key represents + * the alias label and the value represents the alias ID for that alias label. + * This model provides simple access to more well-defined aliases. + */ +export class IdentityModel extends Model { + /** + * The OneSignal ID for this identity. + * WARNING: This *might* be a local ID depending on whether the user has been + * successfully created on the backend or not. + */ + get onesignalId(): string { + return this.getProperty(IdentityConstants.ONESIGNAL_ID); + } + + set onesignalId(value: string) { + this.setProperty(IdentityConstants.ONESIGNAL_ID, value); + } -export type SupportedIdentity = IdentityModel | FutureIdentityModel; + /** + * The (developer-managed) identifier that uniquely identifies this user. + */ + get externalId(): string | null { + return this.getProperty(IdentityConstants.EXTERNAL_ID); + } + + set externalId(value: string | null) { + this.setProperty(IdentityConstants.EXTERNAL_ID, value); + } +} diff --git a/src/core/models/IdentityModelStore.ts b/src/core/models/IdentityModelStore.ts new file mode 100644 index 000000000..3229036cc --- /dev/null +++ b/src/core/models/IdentityModelStore.ts @@ -0,0 +1,13 @@ +import { SimpleModelStore } from '../../shared/models/SimpleModelStore'; +import { SingletonModelStore } from '../../shared/models/SingletonModelStore'; +import { IPreferencesService } from '../../types/preferences'; +import { IdentityModel } from './IdentityModel'; + +/** + * A model store for the Identity model + */ +export class IdentityModelStore extends SingletonModelStore { + constructor(prefs: IPreferencesService) { + super(new SimpleModelStore(() => new IdentityModel(), 'identity', prefs)); + } +} diff --git a/src/onesignal/PushSubscriptionNamespace.ts b/src/onesignal/PushSubscriptionNamespace.ts index 232b36879..39f85dc40 100644 --- a/src/onesignal/PushSubscriptionNamespace.ts +++ b/src/onesignal/PushSubscriptionNamespace.ts @@ -1,3 +1,8 @@ +import { OSModel } from '../core/modelRepo/OSModel'; +import { SupportedSubscription } from '../core/models/SubscriptionModels'; +import { isCompleteSubscriptionObject } from '../core/utils/typePredicates'; +import SubscriptionChangeEvent from '../page/models/SubscriptionChangeEvent'; +import { EventListenerBase } from '../page/userModel/EventListenerBase'; import { ValidatorUtils } from '../page/utils/ValidatorUtils'; import { InvalidArgumentError, @@ -9,17 +14,12 @@ import { } from '../shared/errors/InvalidStateError'; import EventHelper from '../shared/helpers/EventHelper'; import Log from '../shared/libraries/Log'; +import { Subscription } from '../shared/models/Subscription'; import Database from '../shared/services/Database'; import { awaitOneSignalInitAndSupported, logMethodCall, } from '../shared/utils/utils'; -import { SupportedSubscription } from '../core/models/SubscriptionModels'; -import { isCompleteSubscriptionObject } from '../core/utils/typePredicates'; -import { EventListenerBase } from '../page/userModel/EventListenerBase'; -import SubscriptionChangeEvent from '../page/models/SubscriptionChangeEvent'; -import { OSModel } from '../core/modelRepo/OSModel'; -import { Subscription } from '../shared/models/Subscription'; export default class PushSubscriptionNamespace extends EventListenerBase { private _id?: string | null; diff --git a/src/onesignal/User.ts b/src/onesignal/User.ts index 612635716..836f4e9e3 100644 --- a/src/onesignal/User.ts +++ b/src/onesignal/User.ts @@ -1,14 +1,14 @@ import { OSModel } from '../core/modelRepo/OSModel'; -import { ModelName, SupportedModel } from '../core/models/SupportedModels'; import { FutureSubscriptionModel, SubscriptionType, } from '../core/models/SubscriptionModels'; +import { ModelName, SupportedModel } from '../core/models/SupportedModels'; import { InvalidArgumentError, InvalidArgumentReason, } from '../shared/errors/InvalidArgumentError'; -import { logMethodCall, isValidEmail } from '../shared/utils/utils'; +import { isValidEmail, logMethodCall } from '../shared/utils/utils'; import UserDirector from './UserDirector'; export default class User { diff --git a/src/shared/managers/SubscriptionManager.ts b/src/shared/managers/SubscriptionManager.ts index 9f3161ac8..f00463ca9 100644 --- a/src/shared/managers/SubscriptionManager.ts +++ b/src/shared/managers/SubscriptionManager.ts @@ -1,18 +1,24 @@ -import Database from '../services/Database'; import Environment from '../helpers/Environment'; -import OneSignalEvent from '../services/OneSignalEvent'; import { ServiceWorkerActiveState } from '../helpers/ServiceWorkerHelper'; -import SdkEnvironment from './SdkEnvironment'; import { NotificationPermission } from '../models/NotificationPermission'; -import { SubscriptionStateKind } from '../models/SubscriptionStateKind'; -import { WindowEnvironmentKind } from '../models/WindowEnvironmentKind'; import { Subscription } from '../models/Subscription'; -import { UnsubscriptionStrategy } from '../models/UnsubscriptionStrategy'; +import { SubscriptionStateKind } from '../models/SubscriptionStateKind'; import { SubscriptionStrategyKind } from '../models/SubscriptionStrategyKind'; +import { UnsubscriptionStrategy } from '../models/UnsubscriptionStrategy'; +import { WindowEnvironmentKind } from '../models/WindowEnvironmentKind'; +import Database from '../services/Database'; +import OneSignalEvent from '../services/OneSignalEvent'; +import SdkEnvironment from './SdkEnvironment'; -import { PermissionUtils } from '../utils/PermissionUtils'; -import { base64ToUint8Array } from '../utils/Encoding'; -import { ContextSWInterface } from '../models/ContextSW'; +import { OSModel } from '../../core/modelRepo/OSModel'; +import { StringKeys } from '../../core/models/StringKeys'; +import { + FutureSubscriptionModel, + SupportedSubscription, +} from '../../core/models/SubscriptionModels'; +import { isCompleteSubscriptionObject } from '../../core/utils/typePredicates'; +import UserDirector from '../../onesignal/UserDirector'; +import FuturePushSubscriptionRecord from '../../page/userModel/FuturePushSubscriptionRecord'; import { InvalidStateError, InvalidStateReason, @@ -27,20 +33,14 @@ import SubscriptionError, { SubscriptionErrorReason, } from '../errors/SubscriptionError'; import Log from '../libraries/Log'; +import { ContextSWInterface } from '../models/ContextSW'; +import { PushSubscriptionState } from '../models/PushSubscriptionState'; import { RawPushSubscription } from '../models/RawPushSubscription'; -import FuturePushSubscriptionRecord from '../../page/userModel/FuturePushSubscriptionRecord'; -import { - FutureSubscriptionModel, - SupportedSubscription, -} from '../../core/models/SubscriptionModels'; -import { StringKeys } from '../../core/models/StringKeys'; import { SessionOrigin } from '../models/Session'; -import { executeCallback, logMethodCall } from '../utils/utils'; -import UserDirector from '../../onesignal/UserDirector'; -import { OSModel } from '../../core/modelRepo/OSModel'; -import { isCompleteSubscriptionObject } from '../../core/utils/typePredicates'; import { bowserCastle } from '../utils/bowserCastle'; -import { PushSubscriptionState } from '../models/PushSubscriptionState'; +import { base64ToUint8Array } from '../utils/Encoding'; +import { PermissionUtils } from '../utils/PermissionUtils'; +import { executeCallback, logMethodCall } from '../utils/utils'; export const DEFAULT_DEVICE_ID = '99999999-9999-9999-9999-999999999999'; From 8c4da39b24982ac0868931f33aa1259bbe80411c Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 11 Apr 2025 17:16:59 -0700 Subject: [PATCH 3/6] make Operations Folder and add SetAliasOperation --- src/core/executors/ExecutorBase.ts | 2 +- src/core/executors/IdentityExecutor.ts | 2 +- .../executors/IdentityOperationExecutor.ts | 19 +++-- src/core/executors/constants.ts | 2 + src/core/modelRepo/ModelStore.ts | 2 +- src/core/modelRepo/OperationModelStore.ts | 2 +- src/core/models/ExecutorConfig.ts | 2 +- src/core/operationRepo/OperationRepo.test.ts | 28 +++---- src/core/operationRepo/OperationRepo.ts | 17 ++-- src/core/operations/DeleteAliasOperation.ts | 0 .../Operation.ts | 0 src/core/operations/SetAliasOperation.ts | 84 +++++++++++++++++++ src/core/requestService/IdentityRequests.ts | 6 +- .../requestService/SubscriptionRequests.ts | 8 +- .../requestService/UserPropertyRequests.ts | 12 +-- src/core/requestService/helpers.ts | 10 +-- src/shared/managers/IDManager.ts | 27 ++++++ src/types/operation.ts | 36 ++++---- src/types/user.ts | 19 +++++ 19 files changed, 202 insertions(+), 76 deletions(-) create mode 100644 src/core/executors/constants.ts create mode 100644 src/core/operations/DeleteAliasOperation.ts rename src/core/{operationRepo => operations}/Operation.ts (100%) create mode 100644 src/core/operations/SetAliasOperation.ts create mode 100644 src/shared/managers/IDManager.ts create mode 100644 src/types/user.ts diff --git a/src/core/executors/ExecutorBase.ts b/src/core/executors/ExecutorBase.ts index d3e65d4d3..991147236 100644 --- a/src/core/executors/ExecutorBase.ts +++ b/src/core/executors/ExecutorBase.ts @@ -8,7 +8,7 @@ import { CoreChangeType } from '../models/CoreChangeType'; import { CoreDelta } from '../models/CoreDeltas'; import { ExecutorConfig } from '../models/ExecutorConfig'; import { SupportedModel } from '../models/SupportedModels'; -import { Operation } from '../operationRepo/Operation'; +import { Operation } from '../operations/Operation'; import { ExecutorResult } from './ExecutorResult'; const RETRY_AFTER = 5_000; diff --git a/src/core/executors/IdentityExecutor.ts b/src/core/executors/IdentityExecutor.ts index 9b919755e..9a061ccf2 100644 --- a/src/core/executors/IdentityExecutor.ts +++ b/src/core/executors/IdentityExecutor.ts @@ -4,7 +4,7 @@ import { CoreChangeType } from '../models/CoreChangeType'; import { PropertyDelta } from '../models/CoreDeltas'; import { ExecutorConfig } from '../models/ExecutorConfig'; import { ModelName, SupportedModel } from '../models/SupportedModels'; -import { LegacyOperation } from '../operationRepo/LegacyOperation'; +import { Operation } from '../operations/Operation'; import { isPropertyDelta } from '../utils/typePredicates'; import ExecutorBase from './ExecutorBase'; diff --git a/src/core/executors/IdentityOperationExecutor.ts b/src/core/executors/IdentityOperationExecutor.ts index ac7add4d7..afeb67af9 100644 --- a/src/core/executors/IdentityOperationExecutor.ts +++ b/src/core/executors/IdentityOperationExecutor.ts @@ -1,5 +1,12 @@ +import Log from 'src/shared/libraries/Log'; import { IIdentityBackendService } from 'src/types/backend'; -import { IOperationExecutor } from 'src/types/operation'; +import { ExecutionResponse, IOperationExecutor } from 'src/types/operation'; +import { IRebuildUserService } from 'src/types/user'; +import { type IdentityModelStore } from '../models/IdentityModelStore'; +import { type NewRecordsState } from '../operationRepo/NewRecordsState'; +import { type Operation } from '../operations/Operation'; +import { SetAliasOperation } from '../operations/SetAliasOperation'; +import { DELETE_ALIAS, SET_ALIAS } from './constants'; export class IdentityOperationExecutor implements IOperationExecutor { private readonly _identityBackend: IIdentityBackendService; @@ -7,9 +14,6 @@ export class IdentityOperationExecutor implements IOperationExecutor { private readonly _buildUserService: IRebuildUserService; private readonly _newRecordState: NewRecordsState; - static readonly SET_ALIAS = 'set-alias'; - static readonly DELETE_ALIAS = 'delete-alias'; - constructor( identityBackend: IIdentityBackendService, identityModelStore: IdentityModelStore, @@ -23,14 +27,11 @@ export class IdentityOperationExecutor implements IOperationExecutor { } get operations(): string[] { - return [ - IdentityOperationExecutor.SET_ALIAS, - IdentityOperationExecutor.DELETE_ALIAS, - ]; + return [SET_ALIAS, DELETE_ALIAS]; } async execute(operations: Operation[]): Promise { - Logging.debug( + Log.debug( `IdentityOperationExecutor(operations: ${JSON.stringify(operations)})`, ); diff --git a/src/core/executors/constants.ts b/src/core/executors/constants.ts new file mode 100644 index 000000000..e9370139c --- /dev/null +++ b/src/core/executors/constants.ts @@ -0,0 +1,2 @@ +export const SET_ALIAS = 'set-alias'; +export const DELETE_ALIAS = 'delete-alias'; diff --git a/src/core/modelRepo/ModelStore.ts b/src/core/modelRepo/ModelStore.ts index 5cfdbfc83..ebb4a5d5d 100644 --- a/src/core/modelRepo/ModelStore.ts +++ b/src/core/modelRepo/ModelStore.ts @@ -52,7 +52,7 @@ export abstract class ModelStore /** * Create a model from JSON data */ - abstract create(json?: object): TModel | null; + abstract create(json?: object | null): TModel | null; add(model: TModel, tag = ModelChangeTags.NORMAL): void { const oldModel = this.models.find((m) => m.id === model.id); diff --git a/src/core/modelRepo/OperationModelStore.ts b/src/core/modelRepo/OperationModelStore.ts index 015f69b25..a555d02d1 100644 --- a/src/core/modelRepo/OperationModelStore.ts +++ b/src/core/modelRepo/OperationModelStore.ts @@ -1,6 +1,6 @@ import Log from 'src/shared/libraries/Log'; import { IPreferencesService } from 'src/types/preferences'; -import { Operation } from '../operationRepo/Operation'; +import { Operation } from '../operations/Operation'; import { ModelStore } from './ModelStore'; export class OperationModelStore extends ModelStore { diff --git a/src/core/models/ExecutorConfig.ts b/src/core/models/ExecutorConfig.ts index 4aaa04dee..36c518070 100644 --- a/src/core/models/ExecutorConfig.ts +++ b/src/core/models/ExecutorConfig.ts @@ -1,5 +1,5 @@ import { ExecutorResult } from '../executors/ExecutorResult'; -import { Operation } from '../operationRepo/Operation'; +import { Operation } from '../operations/Operation'; import { ModelName, SupportedModel } from './SupportedModels'; export type ExecutorConfig = { diff --git a/src/core/operationRepo/OperationRepo.test.ts b/src/core/operationRepo/OperationRepo.test.ts index d50fd6a5d..b5e9b6cfe 100644 --- a/src/core/operationRepo/OperationRepo.test.ts +++ b/src/core/operationRepo/OperationRepo.test.ts @@ -5,16 +5,16 @@ import { IOperationExecutor, type OperationModelStore, } from '../../types/operation'; +import { + GroupComparisonType, + GroupComparisonValue, + Operation as OperationBase, +} from '../operations/Operation'; import { OP_REPO_EXECUTION_INTERVAL, OP_REPO_POST_CREATE_DELAY, } from './constants'; import { NewRecordsState } from './NewRecordsState'; -import { - GroupComparisonType, - GroupComparisonValue, - Operation as OperationBase, -} from './Operation'; import { OperationQueueItem, OperationRepo } from './OperationRepo'; vi.spyOn(Log, 'error').mockImplementation(() => ''); @@ -176,7 +176,7 @@ describe('OperationRepo', () => { ]; executeFn.mockResolvedValueOnce({ - result: ExecutionResult.Success, + result: ExecutionResult.SUCCESS, operations: additionalOps, }); @@ -212,9 +212,9 @@ describe('OperationRepo', () => { }); test.each([ - ['FailUnauthorized', ExecutionResult.FailUnauthorized], - ['FailNoRetry', ExecutionResult.FailNoRetry], - ['FailConflict', ExecutionResult.FailConflict], + ['FailUnauthorized', ExecutionResult.FAIL_UNAUTHORIZED], + ['FailNoRetry', ExecutionResult.FAIL_NORETRY], + ['FailConflict', ExecutionResult.FAIL_CONFLICT], ])('can handle failed operation: %s', async (_, failResult) => { const opRepo = getNewOpRepo(); const modelRemoveSpy = vi.spyOn(mockOperationModelStore, 'remove'); @@ -233,7 +233,7 @@ describe('OperationRepo', () => { test('can handle success starting only operation', async () => { executeFn.mockResolvedValueOnce({ - result: ExecutionResult.SuccessStartingOnly, + result: ExecutionResult.SUCCESS_STARTING_ONLY, }); const opRepo = getNewOpRepo(); @@ -262,7 +262,7 @@ describe('OperationRepo', () => { test('can handle fail retry operation and delay next execution', async () => { executeFn.mockResolvedValueOnce({ - result: ExecutionResult.FailRetry, + result: ExecutionResult.FAIL_RETRY, retryAfterSeconds: 30, }); @@ -299,7 +299,7 @@ describe('OperationRepo', () => { test('can handle fail pause op repo operation', async () => { executeFn.mockResolvedValueOnce({ - result: ExecutionResult.FailPauseOpRepo, + result: ExecutionResult.FAIL_PAUSE_OPREPO, }); const opRepo = getNewOpRepo(); @@ -332,7 +332,7 @@ describe('OperationRepo', () => { '1': '2', }; executeFn.mockResolvedValueOnce({ - result: ExecutionResult.Success, + result: ExecutionResult.SUCCESS, idTranslations, }); @@ -479,7 +479,7 @@ const mockOperation = new Operation( '123', ); const executeFn: Mock = vi.fn(async () => ({ - result: ExecutionResult.Success, + result: ExecutionResult.SUCCESS, })); const mockExecutor: IOperationExecutor = { diff --git a/src/core/operationRepo/OperationRepo.ts b/src/core/operationRepo/OperationRepo.ts index 620f66aa3..cac9e1d1e 100644 --- a/src/core/operationRepo/OperationRepo.ts +++ b/src/core/operationRepo/OperationRepo.ts @@ -5,17 +5,16 @@ import { IOperationExecutor, IOperationRepo, IStartableService, - Operation, OperationModelStore, } from 'src/types/operation'; import { v4 as uuid } from 'uuid'; +import { GroupComparisonType, type Operation } from '../operations/Operation'; import { OP_REPO_DEFAULT_FAIL_RETRY_BACKOFF, OP_REPO_EXECUTION_INTERVAL, OP_REPO_POST_CREATE_DELAY, } from './constants'; import { type NewRecordsState } from './NewRecordsState'; -import { GroupComparisonType } from './Operation'; // Implements logic similar to Android SDK's OperationRepo & OperationQueueItem // Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationRepo.kt @@ -163,19 +162,19 @@ export class OperationRepo implements IOperationRepo, IStartableService { let highestRetries = 0; switch (response.result) { - case ExecutionResult.Success: + case ExecutionResult.SUCCESS: // Remove operations from store ops.forEach((op) => this.operationModelStore.remove(op.operation.id)); break; - case ExecutionResult.FailUnauthorized: - case ExecutionResult.FailNoRetry: - case ExecutionResult.FailConflict: + case ExecutionResult.FAIL_UNAUTHORIZED: + case ExecutionResult.FAIL_NORETRY: + case ExecutionResult.FAIL_CONFLICT: Log.error(`Operation execution failed without retry: ${operations}`); ops.forEach((op) => this.operationModelStore.remove(op.operation.id)); break; - case ExecutionResult.SuccessStartingOnly: + case ExecutionResult.SUCCESS_STARTING_ONLY: // Remove starting operation and re-add others to the queue this.operationModelStore.remove(startingOp.operation.id); @@ -185,7 +184,7 @@ export class OperationRepo implements IOperationRepo, IStartableService { .forEach((op) => this.queue.unshift(op)); break; - case ExecutionResult.FailRetry: + case ExecutionResult.FAIL_RETRY: Log.error(`Operation execution failed, retrying: ${operations}`); // Add back all operations to front of queue ops.toReversed().forEach((op) => { @@ -197,7 +196,7 @@ export class OperationRepo implements IOperationRepo, IStartableService { }); break; - case ExecutionResult.FailPauseOpRepo: + case ExecutionResult.FAIL_PAUSE_OPREPO: Log.error( `Operation execution failed with eventual retry, pausing the operation repo: ${operations}`, ); diff --git a/src/core/operations/DeleteAliasOperation.ts b/src/core/operations/DeleteAliasOperation.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/core/operationRepo/Operation.ts b/src/core/operations/Operation.ts similarity index 100% rename from src/core/operationRepo/Operation.ts rename to src/core/operations/Operation.ts diff --git a/src/core/operations/SetAliasOperation.ts b/src/core/operations/SetAliasOperation.ts new file mode 100644 index 000000000..412d61db2 --- /dev/null +++ b/src/core/operations/SetAliasOperation.ts @@ -0,0 +1,84 @@ +import { IDManager } from 'src/shared/managers/IDManager'; +import { SET_ALIAS } from '../executors/constants'; +import { + GroupComparisonType, + GroupComparisonValue, + Operation, +} from './Operation'; + +export class SetAliasOperation extends Operation { + constructor(); + constructor(appId: string, onesignalId: string, label: string, value: string); + constructor( + appId?: string, + onesignalId?: string, + label?: string, + value?: string, + ) { + super(SET_ALIAS); + if (appId && onesignalId && label && value) { + this.appId = appId; + this.onesignalId = onesignalId; + this.label = label; + this.value = value; + } + } + + get appId(): string { + return this.getProperty('appId'); + } + + private set appId(value: string) { + this.setProperty('appId', value); + } + + get onesignalId(): string { + return this.getProperty('onesignalId'); + } + + private set onesignalId(value: string) { + this.setProperty('onesignalId', value); + } + + get label(): string { + return this.getProperty('label'); + } + + private set label(value: string) { + this.setProperty('label', value); + } + + get value(): string { + return this.getProperty('value'); + } + + private set value(value: string) { + this.setProperty('value', value); + } + + override get createComparisonKey(): string { + return ''; + } + + override get modifyComparisonKey(): string { + return `${this.appId}.User.${this.onesignalId}.Identity.${this.label}`; + } + + override get groupComparisonType(): GroupComparisonValue { + return GroupComparisonType.ALTER; + } + + override get canStartExecute(): boolean { + return !IDManager.isLocalId(this.onesignalId); + } + + override get applyToRecordId(): string { + return this.onesignalId; + } + + override translateIds(map: Record): void { + if (map[this.onesignalId]) { + this.onesignalId = map[this.onesignalId]; + } + } +} diff --git a/src/core/requestService/IdentityRequests.ts b/src/core/requestService/IdentityRequests.ts index 7ec9155c0..62b84b52d 100644 --- a/src/core/requestService/IdentityRequests.ts +++ b/src/core/requestService/IdentityRequests.ts @@ -1,5 +1,6 @@ -import { logMethodCall } from '../../shared/utils/utils'; import OneSignalError from '../../shared/errors/OneSignalError'; +import MainHelper from '../../shared/helpers/MainHelper'; +import { logMethodCall } from '../../shared/utils/utils'; import { ExecutorResult, ExecutorResultFailNotRetriable, @@ -7,11 +8,10 @@ import { ExecutorResultSuccess, } from '../executors/ExecutorResult'; import { IdentityModel } from '../models/IdentityModel'; -import { Operation } from '../operationRepo/Operation'; +import { Operation } from '../operations/Operation'; import { isIdentityObject } from '../utils/typePredicates'; import { processIdentityOperation } from './helpers'; import { RequestService } from './RequestService'; -import MainHelper from '../../shared/helpers/MainHelper'; /** * This class contains logic for all the Identity model related requests that can be made to the OneSignal API diff --git a/src/core/requestService/SubscriptionRequests.ts b/src/core/requestService/SubscriptionRequests.ts index 5e7643d1d..6f39c255e 100644 --- a/src/core/requestService/SubscriptionRequests.ts +++ b/src/core/requestService/SubscriptionRequests.ts @@ -1,5 +1,6 @@ -import MainHelper from '../../shared/helpers/MainHelper'; +import OneSignalApiBaseResponse from '../../shared/api/OneSignalApiBaseResponse'; import OneSignalError from '../../shared/errors/OneSignalError'; +import MainHelper from '../../shared/helpers/MainHelper'; import { logMethodCall } from '../../shared/utils/utils'; import { ExecutorResult, @@ -11,11 +12,10 @@ import { SubscriptionModel, SupportedSubscription, } from '../models/SubscriptionModels'; -import { Operation } from '../operationRepo/Operation'; +import { Operation } from '../operations/Operation'; +import { isCompleteSubscriptionObject } from '../utils/typePredicates'; import { processSubscriptionOperation } from './helpers'; import { RequestService } from './RequestService'; -import OneSignalApiBaseResponse from '../../shared/api/OneSignalApiBaseResponse'; -import { isCompleteSubscriptionObject } from '../utils/typePredicates'; /** * This class contains logic for all the Subscription model related requests that can be made to the OneSignal API diff --git a/src/core/requestService/UserPropertyRequests.ts b/src/core/requestService/UserPropertyRequests.ts index 494ff0748..69964d866 100644 --- a/src/core/requestService/UserPropertyRequests.ts +++ b/src/core/requestService/UserPropertyRequests.ts @@ -1,5 +1,8 @@ -import { logMethodCall } from '../../shared/utils/utils'; +import OneSignalApiBaseResponse from '../../shared/api/OneSignalApiBaseResponse'; import OneSignalError from '../../shared/errors/OneSignalError'; +import MainHelper from '../../shared/helpers/MainHelper'; +import Log from '../../shared/libraries/Log'; +import { logMethodCall } from '../../shared/utils/utils'; import { ExecutorResult, ExecutorResultFailNotRetriable, @@ -7,13 +10,10 @@ import { ExecutorResultSuccess, } from '../executors/ExecutorResult'; import { UserPropertiesModel } from '../models/UserPropertiesModel'; -import { Operation } from '../operationRepo/Operation'; +import { Operation } from '../operations/Operation'; +import { isCompleteSubscriptionObject } from '../utils/typePredicates'; import AliasPair from './AliasPair'; import { RequestService } from './RequestService'; -import MainHelper from '../../shared/helpers/MainHelper'; -import OneSignalApiBaseResponse from '../../shared/api/OneSignalApiBaseResponse'; -import Log from '../../shared/libraries/Log'; -import { isCompleteSubscriptionObject } from '../utils/typePredicates'; /** * This class contains logic for all the UserProperty model related requests that can be made to the OneSignal API diff --git a/src/core/requestService/helpers.ts b/src/core/requestService/helpers.ts index 0d1cbed8b..f2742072e 100644 --- a/src/core/requestService/helpers.ts +++ b/src/core/requestService/helpers.ts @@ -1,15 +1,15 @@ -import Database from '../../shared/services/Database'; import OneSignalError from '../../shared/errors/OneSignalError'; +import { APIHeaders } from '../../shared/models/APIHeaders'; +import Database from '../../shared/services/Database'; import { IdentityModel } from '../models/IdentityModel'; import { SupportedSubscription } from '../models/SubscriptionModels'; -import { Operation } from '../operationRepo/Operation'; +import { Operation } from '../operations/Operation'; import { - isIdentityObject, - isFutureSubscriptionObject, isCompleteSubscriptionObject, + isFutureSubscriptionObject, + isIdentityObject, } from '../utils/typePredicates'; import AliasPair from './AliasPair'; -import { APIHeaders } from '../../shared/models/APIHeaders'; export function processSubscriptionOperation( operation: Operation, diff --git a/src/shared/managers/IDManager.ts b/src/shared/managers/IDManager.ts new file mode 100644 index 000000000..345c232c8 --- /dev/null +++ b/src/shared/managers/IDManager.ts @@ -0,0 +1,27 @@ +/** + * Manages IDs that are created locally. + * Has the ability to generate globally unique identifiers + * and detect whether a provided ID was generated locally. + */ +export const IDManager = { + LOCAL_PREFIX: 'local-', + + /** + * Create a new local ID to be used temporarily prior to backend generation. + * + * @returns A new locally generated ID. + */ + createLocalId(): string { + return `${this.LOCAL_PREFIX}${crypto.randomUUID()}`; + }, + + /** + * Determine whether the ID provided is locally generated. + * + * @param id - The ID to test. + * @returns True if the ID was created via createLocalId. + */ + isLocalId(id: string): boolean { + return id.startsWith(this.LOCAL_PREFIX); + }, +}; diff --git a/src/types/operation.ts b/src/types/operation.ts index 61d11e892..042529cc9 100644 --- a/src/types/operation.ts +++ b/src/types/operation.ts @@ -1,64 +1,58 @@ +import { type Operation } from 'src/core/operations/Operation'; + // Enums -export enum ExecutionResult { +export const ExecutionResult = { /** * The operation was executed successfully. */ - Success, + SUCCESS: 0, /** * The operation group failed but the starting op should be retried split from the group. */ - SuccessStartingOnly, + SUCCESS_STARTING_ONLY: 1, /** * The operation failed but should be retried. */ - FailRetry, + FAIL_RETRY: 2, /** * The operation failed and should not be tried again. */ - FailNoRetry, + FAIL_NORETRY: 3, /** * The operation failed because the request was not authorized. The operation can be * retried if authorization can be achieved. */ - FailUnauthorized, + FAIL_UNAUTHORIZED: 4, /** * Used in special login case. * The operation failed due to a conflict and can be handled. */ - FailConflict, + FAIL_CONFLICT: 5, /** * Used in special create user case. * The operation failed due to a non-retryable error. Pause the operation repo * and retry on a new session, giving the SDK a chance to recover from the failed user create. */ - FailPauseOpRepo, -} + FAIL_PAUSE_OPREPO: 6, +} as const; -// Interfaces -export interface Operation { - id: string; - name: string; - canStartExecute: boolean; - applyToRecordId?: string; - groupComparisonType: GroupComparisonType; - createComparisonKey: string; - modifyComparisonKey: string; - translateIds(idTranslations: Record): void; -} +export type ExecutionResultValue = + (typeof ExecutionResult)[keyof typeof ExecutionResult]; +// Interfaces export interface IOperationExecutor { operations: string[]; execute(operations: Operation[]): Promise; } export interface ExecutionResponse { - result: ExecutionResult; + result: ExecutionResultValue; operations?: Operation[]; idTranslations?: Record; retryAfterSeconds?: number; diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 000000000..1c22a08c2 --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,19 @@ +import { Operation } from './operation'; + +/** + * Interface for retrieving rebuild operations for a user. + */ +export interface IRebuildUserService { + /** + * Retrieve the list of operations for rebuilding a user, if the + * onesignalId provided represents the current user. + * + * @param appId - The ID of the app. + * @param onesignalId - The ID of the user to retrieve operations for. + * @returns A list of operations if the ID is for the current user, or null otherwise. + */ + getRebuildOperationsIfCurrentUser( + appId: string, + onesignalId: string, + ): Operation[] | null; +} From 6196cb95430a86292814ef25d7fc3f791dae3086 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 11 Apr 2025 17:59:32 -0700 Subject: [PATCH 4/6] update IdentityOperationExecutor Logic --- .../executors/IdentityOperationExecutor.ts | 74 ++++++++++--------- src/core/operations/DeleteAliasOperation.ts | 70 ++++++++++++++++++ src/core/operations/ExecutionResponse.ts | 38 ++++++++++ src/shared/errors/BackendError.ts | 26 +++++++ src/shared/helpers/NetworkUtils.ts | 40 ++++++++++ src/types/backend.ts | 8 +- src/types/operation.ts | 12 +-- src/types/user.ts | 2 +- 8 files changed, 221 insertions(+), 49 deletions(-) create mode 100644 src/core/operations/ExecutionResponse.ts create mode 100644 src/shared/errors/BackendError.ts create mode 100644 src/shared/helpers/NetworkUtils.ts diff --git a/src/core/executors/IdentityOperationExecutor.ts b/src/core/executors/IdentityOperationExecutor.ts index afeb67af9..899867720 100644 --- a/src/core/executors/IdentityOperationExecutor.ts +++ b/src/core/executors/IdentityOperationExecutor.ts @@ -1,9 +1,17 @@ +import { BackendError } from 'src/shared/errors/BackendError'; +import { + getResponseStatusType, + ResponseStatusType, +} from 'src/shared/helpers/NetworkUtils'; import Log from 'src/shared/libraries/Log'; -import { IIdentityBackendService } from 'src/types/backend'; -import { ExecutionResponse, IOperationExecutor } from 'src/types/operation'; +import { IdentityConstants, IIdentityBackendService } from 'src/types/backend'; +import { ModelChangeTags } from 'src/types/models'; +import { ExecutionResult, IOperationExecutor } from 'src/types/operation'; import { IRebuildUserService } from 'src/types/user'; import { type IdentityModelStore } from '../models/IdentityModelStore'; import { type NewRecordsState } from '../operationRepo/NewRecordsState'; +import { DeleteAliasOperation } from '../operations/DeleteAliasOperation'; +import { ExecutionResponse } from '../operations/ExecutionResponse'; import { type Operation } from '../operations/Operation'; import { SetAliasOperation } from '../operations/SetAliasOperation'; import { DELETE_ALIAS, SET_ALIAS } from './constants'; @@ -76,37 +84,33 @@ export class IdentityOperationExecutor implements IOperationExecutor { this._identityModelStore.model.onesignalId === lastOperation.onesignalId ) { - this._identityModelStore.model.setStringProperty( + this._identityModelStore.model.setProperty( lastOperation.label, lastOperation.value, ModelChangeTags.HYDRATE, ); } } catch (ex) { - if (ex instanceof BackendException) { - const responseType = NetworkUtils.getResponseStatusType( - ex.statusCode, - ); + if (ex instanceof BackendError) { + const responseType = getResponseStatusType(ex.statusCode); switch (responseType) { - case NetworkUtils.ResponseStatusType.RETRYABLE: + case ResponseStatusType.RETRYABLE: return new ExecutionResponse( ExecutionResult.FAIL_RETRY, ex.retryAfterSeconds, ); - case NetworkUtils.ResponseStatusType.INVALID: + + case ResponseStatusType.INVALID: return new ExecutionResponse(ExecutionResult.FAIL_NORETRY); - case NetworkUtils.ResponseStatusType.CONFLICT: + + case ResponseStatusType.CONFLICT: return new ExecutionResponse( ExecutionResult.FAIL_CONFLICT, ex.retryAfterSeconds, ); - case NetworkUtils.ResponseStatusType.UNAUTHORIZED: - return new ExecutionResponse( - ExecutionResult.FAIL_UNAUTHORIZED, - ex.retryAfterSeconds, - ); - case NetworkUtils.ResponseStatusType.MISSING: + + case ResponseStatusType.MISSING: { if ( ex.statusCode === 404 && this._newRecordState.isInMissingRetryWindow( @@ -126,13 +130,13 @@ export class IdentityOperationExecutor implements IOperationExecutor { ); if (!rebuildOps) { return new ExecutionResponse(ExecutionResult.FAIL_NORETRY); - } else { - return new ExecutionResponse( - ExecutionResult.FAIL_RETRY, - ex.retryAfterSeconds, - rebuildOps, - ); } + return new ExecutionResponse( + ExecutionResult.FAIL_RETRY, + ex.retryAfterSeconds, + rebuildOps, + ); + } } } } @@ -149,34 +153,35 @@ export class IdentityOperationExecutor implements IOperationExecutor { this._identityModelStore.model.onesignalId === lastOperation.onesignalId ) { - this._identityModelStore.model.setOptStringProperty( + this._identityModelStore.model.setProperty( lastOperation.label, null, ModelChangeTags.HYDRATE, ); } } catch (ex) { - if (ex instanceof BackendException) { - const responseType = NetworkUtils.getResponseStatusType( - ex.statusCode, - ); + if (ex instanceof BackendError) { + const responseType = getResponseStatusType(ex.statusCode); switch (responseType) { - case NetworkUtils.ResponseStatusType.RETRYABLE: + case ResponseStatusType.RETRYABLE: return new ExecutionResponse( ExecutionResult.FAIL_RETRY, ex.retryAfterSeconds, ); - case NetworkUtils.ResponseStatusType.CONFLICT: + case ResponseStatusType.CONFLICT: return new ExecutionResponse(ExecutionResult.SUCCESS); // alias doesn’t exist = good - case NetworkUtils.ResponseStatusType.INVALID: + + case ResponseStatusType.INVALID: return new ExecutionResponse(ExecutionResult.FAIL_NORETRY); - case NetworkUtils.ResponseStatusType.UNAUTHORIZED: + + case ResponseStatusType.UNAUTHORIZED: return new ExecutionResponse( ExecutionResult.FAIL_UNAUTHORIZED, ex.retryAfterSeconds, ); - case NetworkUtils.ResponseStatusType.MISSING: + + case ResponseStatusType.MISSING: if ( ex.statusCode === 404 && this._newRecordState.isInMissingRetryWindow( @@ -188,7 +193,10 @@ export class IdentityOperationExecutor implements IOperationExecutor { ex.retryAfterSeconds, ); } else { - return new ExecutionResponse(ExecutionResult.SUCCESS); // already deleted + // This means either the User or the Alias was already + // deleted, either way the end state is the same, the + // alias no longer exists on that User. + return new ExecutionResponse(ExecutionResult.SUCCESS); } } } diff --git a/src/core/operations/DeleteAliasOperation.ts b/src/core/operations/DeleteAliasOperation.ts index e69de29bb..632a744cd 100644 --- a/src/core/operations/DeleteAliasOperation.ts +++ b/src/core/operations/DeleteAliasOperation.ts @@ -0,0 +1,70 @@ +import { IDManager } from 'src/shared/managers/IDManager'; +import { DELETE_ALIAS } from '../executors/constants'; +import { + GroupComparisonType, + GroupComparisonValue, + Operation, +} from './Operation'; + +export class DeleteAliasOperation extends Operation { + constructor(); + constructor(appId: string, onesignalId: string, label: string); + constructor(appId?: string, onesignalId?: string, label?: string) { + super(DELETE_ALIAS); + if (appId && onesignalId && label) { + this.appId = appId; + this.onesignalId = onesignalId; + this.label = label; + } + } + + get appId(): string { + return this.getProperty('appId'); + } + + private set appId(value: string) { + this.setProperty('appId', value); + } + + get onesignalId(): string { + return this.getProperty('onesignalId'); + } + + private set onesignalId(value: string) { + this.setProperty('onesignalId', value); + } + + get label(): string { + return this.getProperty('label'); + } + + private set label(value: string) { + this.setProperty('label', value); + } + + override get createComparisonKey(): string { + return ''; + } + + override get modifyComparisonKey(): string { + return `${this.appId}.User.${this.onesignalId}.Alias.${this.label}`; + } + + override get groupComparisonType(): GroupComparisonValue { + return GroupComparisonType.NONE; + } + + override get canStartExecute(): boolean { + return !IDManager.isLocalId(this.onesignalId); + } + + override get applyToRecordId(): string { + return this.onesignalId; + } + + override translateIds(map: Record): void { + if (map[this.onesignalId]) { + this.onesignalId = map[this.onesignalId]; + } + } +} diff --git a/src/core/operations/ExecutionResponse.ts b/src/core/operations/ExecutionResponse.ts new file mode 100644 index 000000000..a01aef674 --- /dev/null +++ b/src/core/operations/ExecutionResponse.ts @@ -0,0 +1,38 @@ +import { ExecutionResultValue } from 'src/types/operation'; +import { type Operation } from './Operation'; + +export class ExecutionResponse { + /** + * The result of the execution + */ + result: ExecutionResultValue; + + /** + * The map of id translations that should be applied to any outstanding operations. + * Within the map the key is the local Id, the value is the remote Id. + */ + idTranslations?: Record; + + /** + * When specified, any operations that should be prepended to the operation repo. + */ + operations?: Operation[]; + + /** + * Optional Integer value maybe returned from the backend. + * The module handing this should delay any future requests by this time. + */ + retryAfterSeconds?: number; + + constructor( + result: ExecutionResultValue, + retryAfterSeconds?: number, + operations?: Operation[], + idTranslations?: Record, + ) { + this.result = result; + this.retryAfterSeconds = retryAfterSeconds; + this.operations = operations; + this.idTranslations = idTranslations; + } +} diff --git a/src/shared/errors/BackendError.ts b/src/shared/errors/BackendError.ts new file mode 100644 index 000000000..75a657d4c --- /dev/null +++ b/src/shared/errors/BackendError.ts @@ -0,0 +1,26 @@ +import OneSignalError from './OneSignalError'; + +// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/main/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/exceptions/BackendException.kt +/** + * Raised when a backend service request has failed. + */ +export class BackendError extends OneSignalError { + public readonly statusCode: number; + public readonly response?: string; + public readonly retryAfterSeconds?: number; + + constructor( + statusCode: number, + response?: string, + retryAfterSeconds?: number, + ) { + super(`Backend request failed with status ${statusCode}`); + this.name = 'BackendError'; + this.statusCode = statusCode; + this.response = response; + this.retryAfterSeconds = retryAfterSeconds; + + // Required for extending built-in classes in ES5+ + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/src/shared/helpers/NetworkUtils.ts b/src/shared/helpers/NetworkUtils.ts new file mode 100644 index 000000000..805cc296c --- /dev/null +++ b/src/shared/helpers/NetworkUtils.ts @@ -0,0 +1,40 @@ +export const ResponseStatusType = { + INVALID: 'INVALID', + RETRYABLE: 'RETRYABLE', + UNAUTHORIZED: 'UNAUTHORIZED', + MISSING: 'MISSING', + CONFLICT: 'CONFLICT', +} as const; + +type ResponseStatusValue = + (typeof ResponseStatusType)[keyof typeof ResponseStatusType]; + +export const MAX_NETWORK_REQUEST_ATTEMPT_COUNT = 3; + +/** + * Determines the response status type based on HTTP status code. + * @param statusCode - The HTTP status code. + * @returns ResponseStatusType indicating how the system should handle the error. + */ +export function getResponseStatusType(statusCode: number): ResponseStatusValue { + switch (statusCode) { + case 400: + case 402: + return ResponseStatusType.INVALID; + + case 401: + case 403: + return ResponseStatusType.UNAUTHORIZED; + + case 404: + case 410: + return ResponseStatusType.MISSING; + + case 409: + return ResponseStatusType.CONFLICT; + + case 429: + default: + return ResponseStatusType.RETRYABLE; + } +} diff --git a/src/types/backend.ts b/src/types/backend.ts index b4a7188da..b6207ee28 100644 --- a/src/types/backend.ts +++ b/src/types/backend.ts @@ -1,13 +1,13 @@ export interface IIdentityBackendService { /** * Set one or more aliases for the user identified by the aliasLabel/aliasValue provided. - * If there is a non-successful response from the backend, a BackendException should be thrown with response data. + * If there is a non-successful response from the backend, a BackendError should be thrown with response data. * * @param appId - The ID of the OneSignal application this user exists under. * @param aliasLabel - The alias label to retrieve the user under. * @param aliasValue - The identifier within the aliasLabel that identifies the user to retrieve. * @param identities - The identities that are to be created. - * @throws BackendException + * @throws BackendError */ setAlias( appId: string, @@ -18,13 +18,13 @@ export interface IIdentityBackendService { /** * Delete the aliasLabelToDelete from the user identified by the aliasLabel/aliasValue provided. - * If there is a non-successful response from the backend, a BackendException should be thrown with response data. + * If there is a non-successful response from the backend, a BackendError should be thrown with response data. * * @param appId - The ID of the OneSignal application this user exists under. * @param aliasLabel - The alias label to retrieve the user under. * @param aliasValue - The identifier within the aliasLabel that identifies the user to retrieve. * @param aliasLabelToDelete - The alias label to delete from the user identified. - * @throws BackendException + * @throws BackendError */ deleteAlias( appId: string, diff --git a/src/types/operation.ts b/src/types/operation.ts index 042529cc9..a44032c3a 100644 --- a/src/types/operation.ts +++ b/src/types/operation.ts @@ -1,3 +1,4 @@ +import { type ExecutionResponse } from 'src/core/operations/ExecutionResponse'; import { type Operation } from 'src/core/operations/Operation'; // Enums @@ -51,17 +52,6 @@ export interface IOperationExecutor { execute(operations: Operation[]): Promise; } -export interface ExecutionResponse { - result: ExecutionResultValue; - operations?: Operation[]; - idTranslations?: Record; - retryAfterSeconds?: number; -} - -export interface ITime { - getCurrentTimeMillis(): number; -} - export interface ConfigModel { opRepoExecutionInterval: number; opRepoPostWakeDelay: number; diff --git a/src/types/user.ts b/src/types/user.ts index 1c22a08c2..816a0ca4e 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -1,4 +1,4 @@ -import { Operation } from './operation'; +import { type Operation } from 'src/core/operations/Operation'; /** * Interface for retrieving rebuild operations for a user. From cf47f1deb71f0152658f47295a01d2436d101b22 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 11 Apr 2025 18:08:04 -0700 Subject: [PATCH 5/6] update operation modal store to handle new operations --- src/core/executors/IdentityExecutor.ts | 3 ++- .../executors/IdentityOperationExecutor.ts | 6 +++-- src/core/executors/constants.ts | 6 +++-- src/core/modelRepo/ModelStore.ts | 17 ++++++++++---- src/core/modelRepo/OperationModelStore.ts | 22 ++++++++++++------- src/core/models/IdentityModelStore.ts | 5 ++--- src/core/operations/DeleteAliasOperation.ts | 8 +++---- src/core/operations/SetAliasOperation.ts | 9 ++++---- src/shared/errors/BackendError.ts | 3 ++- src/shared/models/SimpleModelStore.ts | 4 +++- src/shared/models/SingletonModelStore.ts | 17 ++++++++++---- src/types/models.ts | 3 +++ src/types/preferences.ts | 4 +--- 13 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/core/executors/IdentityExecutor.ts b/src/core/executors/IdentityExecutor.ts index 9a061ccf2..bf996d451 100644 --- a/src/core/executors/IdentityExecutor.ts +++ b/src/core/executors/IdentityExecutor.ts @@ -4,10 +4,11 @@ import { CoreChangeType } from '../models/CoreChangeType'; import { PropertyDelta } from '../models/CoreDeltas'; import { ExecutorConfig } from '../models/ExecutorConfig'; import { ModelName, SupportedModel } from '../models/SupportedModels'; -import { Operation } from '../operations/Operation'; +import { LegacyOperation } from '../operationRepo/LegacyOperation'; import { isPropertyDelta } from '../utils/typePredicates'; import ExecutorBase from './ExecutorBase'; +// TODO: Remove this with later Web SDK Prs export class IdentityExecutor extends ExecutorBase { constructor( executorConfig: ExecutorConfig, diff --git a/src/core/executors/IdentityOperationExecutor.ts b/src/core/executors/IdentityOperationExecutor.ts index 899867720..f9b8e99fe 100644 --- a/src/core/executors/IdentityOperationExecutor.ts +++ b/src/core/executors/IdentityOperationExecutor.ts @@ -14,8 +14,10 @@ import { DeleteAliasOperation } from '../operations/DeleteAliasOperation'; import { ExecutionResponse } from '../operations/ExecutionResponse'; import { type Operation } from '../operations/Operation'; import { SetAliasOperation } from '../operations/SetAliasOperation'; -import { DELETE_ALIAS, SET_ALIAS } from './constants'; +import { OPERATION_NAME } from './constants'; +// Implements logic similar to Android SDK's IdentityOperationExecutor +// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/IdentityOperationExecutor.kt export class IdentityOperationExecutor implements IOperationExecutor { private readonly _identityBackend: IIdentityBackendService; private readonly _identityModelStore: IdentityModelStore; @@ -35,7 +37,7 @@ export class IdentityOperationExecutor implements IOperationExecutor { } get operations(): string[] { - return [SET_ALIAS, DELETE_ALIAS]; + return [OPERATION_NAME.SET_ALIAS, OPERATION_NAME.DELETE_ALIAS]; } async execute(operations: Operation[]): Promise { diff --git a/src/core/executors/constants.ts b/src/core/executors/constants.ts index e9370139c..53c46c517 100644 --- a/src/core/executors/constants.ts +++ b/src/core/executors/constants.ts @@ -1,2 +1,4 @@ -export const SET_ALIAS = 'set-alias'; -export const DELETE_ALIAS = 'delete-alias'; +export const OPERATION_NAME = { + SET_ALIAS: 'set-alias', + DELETE_ALIAS: 'delete-alias', +} as const; diff --git a/src/core/modelRepo/ModelStore.ts b/src/core/modelRepo/ModelStore.ts index ebb4a5d5d..b1c5630db 100644 --- a/src/core/modelRepo/ModelStore.ts +++ b/src/core/modelRepo/ModelStore.ts @@ -3,7 +3,12 @@ import { EventProducer } from 'src/shared/helpers/EventProducer'; import Log from 'src/shared/libraries/Log'; import type { IEventNotifier } from 'src/types/events'; -import type { IModelStore, IModelStoreChangeHandler } from 'src/types/models'; +import { + ModelChangeTags, + ModelChangeTagValue, + type IModelStore, + type IModelStoreChangeHandler, +} from 'src/types/models'; import type { IPreferencesService } from 'src/types/preferences'; import type { IModelChangedHandler, @@ -54,13 +59,17 @@ export abstract class ModelStore */ abstract create(json?: object | null): TModel | null; - add(model: TModel, tag = ModelChangeTags.NORMAL): void { + add(model: TModel, tag: ModelChangeTagValue = ModelChangeTags.NORMAL): void { const oldModel = this.models.find((m) => m.id === model.id); if (oldModel) this.removeItem(oldModel, tag); this.addItem(model, tag); } - addAt(index: number, model: TModel, tag = ModelChangeTags.NORMAL): void { + addAt( + index: number, + model: TModel, + tag: ModelChangeTagValue = ModelChangeTags.NORMAL, + ): void { const oldModel = this.models.find((m) => m.id === model.id); if (oldModel) this.removeItem(oldModel, tag); this.addItem(model, tag, index); @@ -90,7 +99,7 @@ export abstract class ModelStore ); } - replaceAll(newModels: TModel[], tag: string): void { + replaceAll(newModels: TModel[], tag: ModelChangeTagValue): void { this.clear(tag); for (const model of newModels) { this.add(model, tag); diff --git a/src/core/modelRepo/OperationModelStore.ts b/src/core/modelRepo/OperationModelStore.ts index a555d02d1..3f19ad11d 100644 --- a/src/core/modelRepo/OperationModelStore.ts +++ b/src/core/modelRepo/OperationModelStore.ts @@ -1,6 +1,9 @@ import Log from 'src/shared/libraries/Log'; import { IPreferencesService } from 'src/types/preferences'; +import { OPERATION_NAME } from '../executors/constants'; +import { DeleteAliasOperation } from '../operations/DeleteAliasOperation'; import { Operation } from '../operations/Operation'; +import { SetAliasOperation } from '../operations/SetAliasOperation'; import { ModelStore } from './ModelStore'; export class OperationModelStore extends ModelStore { @@ -23,20 +26,23 @@ export class OperationModelStore extends ModelStore { } // Determine the type of operation based on the name property in the json + const operationName = jsonObject.name; let operation: Operation; - // const operationName = jsonObject.name; - // TODO: add in executors in later prs - // switch (operationName) { - // default: - // throw new Error(`Unrecognized operation: ${operationName}`); - // } + switch (operationName) { + case OPERATION_NAME.SET_ALIAS: + operation = new SetAliasOperation(); + break; + case OPERATION_NAME.DELETE_ALIAS: + operation = new DeleteAliasOperation(); + break; + default: + throw new Error(`Unrecognized operation: ${operationName}`); + } // populate the operation with the data - // @ts-expect-error - TODO: add in executors in later prs operation.initializeFromJson(jsonObject); - // @ts-expect-error - TODO: add in executors in later prs return operation; } diff --git a/src/core/models/IdentityModelStore.ts b/src/core/models/IdentityModelStore.ts index 3229036cc..4e83b63db 100644 --- a/src/core/models/IdentityModelStore.ts +++ b/src/core/models/IdentityModelStore.ts @@ -3,9 +3,8 @@ import { SingletonModelStore } from '../../shared/models/SingletonModelStore'; import { IPreferencesService } from '../../types/preferences'; import { IdentityModel } from './IdentityModel'; -/** - * A model store for the Identity model - */ +// Implements logic similar to Android SDK's IdentityModelStore +// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/identity/IdentityModelStore.kt export class IdentityModelStore extends SingletonModelStore { constructor(prefs: IPreferencesService) { super(new SimpleModelStore(() => new IdentityModel(), 'identity', prefs)); diff --git a/src/core/operations/DeleteAliasOperation.ts b/src/core/operations/DeleteAliasOperation.ts index 632a744cd..9746c9cd5 100644 --- a/src/core/operations/DeleteAliasOperation.ts +++ b/src/core/operations/DeleteAliasOperation.ts @@ -1,16 +1,16 @@ import { IDManager } from 'src/shared/managers/IDManager'; -import { DELETE_ALIAS } from '../executors/constants'; +import { OPERATION_NAME } from '../executors/constants'; import { GroupComparisonType, GroupComparisonValue, Operation, } from './Operation'; +// Implements logic similar to Android SDK's DeleteAliasOperation +// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteAliasOperation.kt export class DeleteAliasOperation extends Operation { - constructor(); - constructor(appId: string, onesignalId: string, label: string); constructor(appId?: string, onesignalId?: string, label?: string) { - super(DELETE_ALIAS); + super(OPERATION_NAME.DELETE_ALIAS); if (appId && onesignalId && label) { this.appId = appId; this.onesignalId = onesignalId; diff --git a/src/core/operations/SetAliasOperation.ts b/src/core/operations/SetAliasOperation.ts index 412d61db2..708fbac30 100644 --- a/src/core/operations/SetAliasOperation.ts +++ b/src/core/operations/SetAliasOperation.ts @@ -1,21 +1,22 @@ import { IDManager } from 'src/shared/managers/IDManager'; -import { SET_ALIAS } from '../executors/constants'; +import { OPERATION_NAME } from '../executors/constants'; + import { GroupComparisonType, GroupComparisonValue, Operation, } from './Operation'; +// Implements logic similar to Android SDK's SetAliasOperation +// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetAliasOperation.kt export class SetAliasOperation extends Operation { - constructor(); - constructor(appId: string, onesignalId: string, label: string, value: string); constructor( appId?: string, onesignalId?: string, label?: string, value?: string, ) { - super(SET_ALIAS); + super(OPERATION_NAME.SET_ALIAS); if (appId && onesignalId && label && value) { this.appId = appId; this.onesignalId = onesignalId; diff --git a/src/shared/errors/BackendError.ts b/src/shared/errors/BackendError.ts index 75a657d4c..324c2686e 100644 --- a/src/shared/errors/BackendError.ts +++ b/src/shared/errors/BackendError.ts @@ -1,6 +1,7 @@ import OneSignalError from './OneSignalError'; -// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/main/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/exceptions/BackendException.kt +// Implements logic similar to Android SDK's BackendException +// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/exceptions/BackendException.kt /** * Raised when a backend service request has failed. */ diff --git a/src/shared/models/SimpleModelStore.ts b/src/shared/models/SimpleModelStore.ts index 6114a7534..a5c71adda 100644 --- a/src/shared/models/SimpleModelStore.ts +++ b/src/shared/models/SimpleModelStore.ts @@ -1,7 +1,9 @@ -import { Model } from 'src/core/modelRepo/Model'; import { ModelStore } from 'src/core/modelRepo/ModelStore'; +import { Model } from 'src/core/models/Model'; import { IPreferencesService } from 'src/types/preferences'; +// Implements logic similar to Android SDK's SimpleModelStore +// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/SimpleModelStore.kt /** * A simple model store is a concrete implementation of the ModelStore, * which provides a basic create() method using the passed-in factory. diff --git a/src/shared/models/SingletonModelStore.ts b/src/shared/models/SingletonModelStore.ts index f3a1fa3bb..22e1f0b0e 100644 --- a/src/shared/models/SingletonModelStore.ts +++ b/src/shared/models/SingletonModelStore.ts @@ -1,5 +1,5 @@ -import { Model, ModelChangedArgs } from 'src/core/modelRepo/Model'; import { ModelStore } from 'src/core/modelRepo/ModelStore'; +import { Model, ModelChangedArgs } from 'src/core/models/Model'; import { IModelStoreChangeHandler, ISingletonModelStore, @@ -7,6 +7,8 @@ import { } from 'src/types/models'; import { EventProducer } from '../helpers/EventProducer'; +// Implements logic similar to Android SDK's SingletonModelStore +// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/SingletonModelStore.kt export class SingletonModelStore implements ISingletonModelStore, IModelStoreChangeHandler { @@ -55,8 +57,11 @@ export class SingletonModelStore return this.changeSubscription.hasSubscribers; } - // These are no-ops by design - onModelAdded(model: TModel, tag: string): void { + /** + * @param {TModel} model + * @param {string} tag + */ + onModelAdded(): void { // No-op: singleton is transparently added } @@ -66,7 +71,11 @@ export class SingletonModelStore ); } - onModelRemoved(model: TModel, tag: string): void { + /** + * @param {TModel} model + * @param {string} tag + */ + onModelRemoved(): void { // No-op: singleton is never removed } } diff --git a/src/types/models.ts b/src/types/models.ts index 595a57497..be241f37e 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -7,6 +7,9 @@ export const ModelChangeTags = { HYDRATE: 'HYDRATE', } as const; +export type ModelChangeTagValue = + (typeof ModelChangeTags)[keyof typeof ModelChangeTags]; + /** * A handler interface for subscribing to model change events for a specific model store. */ diff --git a/src/types/preferences.ts b/src/types/preferences.ts index 052d98672..bab1021bb 100644 --- a/src/types/preferences.ts +++ b/src/types/preferences.ts @@ -1,7 +1,5 @@ /** - * Provides access to the low level preferences. There are one or more preference - * stores, identified by PreferenceStores, each store contains a key for each - * preference. Each key has a known data type, it's value can be fetched/stored as + * Provides access to the low level preferences. Each key has a known data type, it's value can be fetched/stored as * needed. Stored preferences will persist across the lifetime of the app installation. */ export interface IPreferencesService { From 15cd3607d294ba1909727fc50bf1d9b9610e2a8b Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 14 Apr 2025 16:10:48 -0700 Subject: [PATCH 6/6] clean up --- __test__/support/helpers/core.ts | 8 ++--- src/core/CoreModuleDirector.ts | 9 ++---- src/core/models/IdentityModel.ts | 2 +- src/core/models/SupportedModels.ts | 4 +-- src/core/models/UserData.ts | 9 ++++-- src/core/operations/DeleteAliasOperation.ts | 13 +++----- src/core/operations/SetAliasOperation.ts | 22 +++++-------- src/core/requestService/CreateUserPayload.ts | 4 +-- src/core/requestService/RequestService.ts | 34 ++++++++++---------- src/onesignal/UserDirector.ts | 12 +++---- src/page/managers/LoginManager.ts | 9 +++--- src/shared/helpers/NetworkUtils.ts | 4 +-- 12 files changed, 58 insertions(+), 72 deletions(-) diff --git a/__test__/support/helpers/core.ts b/__test__/support/helpers/core.ts index 0960308aa..72b3e3aa4 100644 --- a/__test__/support/helpers/core.ts +++ b/__test__/support/helpers/core.ts @@ -1,9 +1,9 @@ +import { Identity } from 'src/core/models/UserData'; import CoreModule from '../../../src/core/CoreModule'; import { CoreModuleDirector } from '../../../src/core/CoreModuleDirector'; import { OSModel } from '../../../src/core/modelRepo/OSModel'; import { CoreChangeType } from '../../../src/core/models/CoreChangeType'; import { CoreDelta } from '../../../src/core/models/CoreDeltas'; -import { SupportedIdentity } from '../../../src/core/models/IdentityModel'; import { SubscriptionType, SupportedSubscription, @@ -28,7 +28,7 @@ export function generateNewSubscription(modelId = '0000000000') { ); } -export function getMockDeltas(): CoreDelta[] { +export function getMockDeltas(): CoreDelta[] { return [ { model: getDummyIdentityOSModel(), @@ -39,8 +39,8 @@ export function getMockDeltas(): CoreDelta[] { export function getDummyIdentityOSModel( modelId = DUMMY_MODEL_ID, -): OSModel { - return new OSModel(ModelName.Identity, {}, modelId); +): OSModel { + return new OSModel(ModelName.Identity, {}, modelId); } export function getDummyPropertyOSModel( diff --git a/src/core/CoreModuleDirector.ts b/src/core/CoreModuleDirector.ts index 73cebd1a1..8bb1e4f0d 100644 --- a/src/core/CoreModuleDirector.ts +++ b/src/core/CoreModuleDirector.ts @@ -12,7 +12,6 @@ import Database from '../shared/services/Database'; import { logMethodCall } from '../shared/utils/utils'; import CoreModule from './CoreModule'; import { OSModel } from './modelRepo/OSModel'; -import { SupportedIdentity } from './models/IdentityModel'; import { ModelStoresMap } from './models/ModelStoresMap'; import { SubscriptionChannel, @@ -21,7 +20,7 @@ import { SupportedSubscription, } from './models/SubscriptionModels'; import { ModelName, SupportedModel } from './models/SupportedModels'; -import UserData from './models/UserData'; +import UserData, { Identity } from './models/UserData'; import { UserPropertiesModel } from './models/UserPropertiesModel'; /* Contains OneSignal User-Model-specific logic*/ @@ -307,13 +306,11 @@ export class CoreModuleDirector { ); } - public getIdentityModel(): OSModel | undefined { + public getIdentityModel(): OSModel | undefined { logMethodCall('CoreModuleDirector.getIdentityModel'); const modelStores = this.getModelStores(); const modelKeys = Object.keys(modelStores.identity.models); - return modelStores.identity.models[ - modelKeys[0] - ] as OSModel; + return modelStores.identity.models[modelKeys[0]] as OSModel; } public getPropertiesModel(): OSModel | undefined { diff --git a/src/core/models/IdentityModel.ts b/src/core/models/IdentityModel.ts index 629ef1ddc..b14392f7c 100644 --- a/src/core/models/IdentityModel.ts +++ b/src/core/models/IdentityModel.ts @@ -1,5 +1,5 @@ import { IdentityConstants } from 'src/types/backend'; -import { Model } from '../modelRepo/Model'; +import { Model } from './Model'; /** * The identity model as a MapModel: a simple key-value pair where the key represents diff --git a/src/core/models/SupportedModels.ts b/src/core/models/SupportedModels.ts index cb4470b6b..2884955e7 100644 --- a/src/core/models/SupportedModels.ts +++ b/src/core/models/SupportedModels.ts @@ -1,5 +1,5 @@ -import { SupportedIdentity } from './IdentityModel'; import { SupportedSubscription } from './SubscriptionModels'; +import { Identity } from './UserData'; import { UserPropertiesModel } from './UserPropertiesModel'; export enum ModelName { @@ -15,6 +15,6 @@ export enum LegacyModelName { } export type SupportedModel = - | SupportedIdentity + | Identity | UserPropertiesModel | SupportedSubscription; diff --git a/src/core/models/UserData.ts b/src/core/models/UserData.ts index e43ec20d6..439707548 100644 --- a/src/core/models/UserData.ts +++ b/src/core/models/UserData.ts @@ -1,10 +1,15 @@ -import { SupportedIdentity } from './IdentityModel'; import { SupportedSubscription } from './SubscriptionModels'; import { UserPropertiesModel } from './UserPropertiesModel'; +export interface Identity { + onesignal_id?: string; + external_id?: string; + [key: string]: unknown; +} + type UserData = { properties: UserPropertiesModel; - identity: SupportedIdentity; + identity: Identity; subscriptions?: SupportedSubscription[]; }; diff --git a/src/core/operations/DeleteAliasOperation.ts b/src/core/operations/DeleteAliasOperation.ts index 9746c9cd5..b73047837 100644 --- a/src/core/operations/DeleteAliasOperation.ts +++ b/src/core/operations/DeleteAliasOperation.ts @@ -9,19 +9,16 @@ import { // Implements logic similar to Android SDK's DeleteAliasOperation // Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/DeleteAliasOperation.kt export class DeleteAliasOperation extends Operation { - constructor(appId?: string, onesignalId?: string, label?: string) { + constructor(appId: string, onesignalId: string, label: string) { super(OPERATION_NAME.DELETE_ALIAS); - if (appId && onesignalId && label) { - this.appId = appId; - this.onesignalId = onesignalId; - this.label = label; - } + this.appId = appId; + this.onesignalId = onesignalId; + this.label = label; } get appId(): string { return this.getProperty('appId'); } - private set appId(value: string) { this.setProperty('appId', value); } @@ -29,7 +26,6 @@ export class DeleteAliasOperation extends Operation { get onesignalId(): string { return this.getProperty('onesignalId'); } - private set onesignalId(value: string) { this.setProperty('onesignalId', value); } @@ -37,7 +33,6 @@ export class DeleteAliasOperation extends Operation { get label(): string { return this.getProperty('label'); } - private set label(value: string) { this.setProperty('label', value); } diff --git a/src/core/operations/SetAliasOperation.ts b/src/core/operations/SetAliasOperation.ts index 708fbac30..c817cf2bb 100644 --- a/src/core/operations/SetAliasOperation.ts +++ b/src/core/operations/SetAliasOperation.ts @@ -11,24 +11,21 @@ import { // Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/SetAliasOperation.kt export class SetAliasOperation extends Operation { constructor( - appId?: string, - onesignalId?: string, - label?: string, - value?: string, + appId: string, + onesignalId: string, + label: string, + value: string, ) { super(OPERATION_NAME.SET_ALIAS); - if (appId && onesignalId && label && value) { - this.appId = appId; - this.onesignalId = onesignalId; - this.label = label; - this.value = value; - } + this.appId = appId; + this.onesignalId = onesignalId; + this.label = label; + this.value = value; } get appId(): string { return this.getProperty('appId'); } - private set appId(value: string) { this.setProperty('appId', value); } @@ -36,7 +33,6 @@ export class SetAliasOperation extends Operation { get onesignalId(): string { return this.getProperty('onesignalId'); } - private set onesignalId(value: string) { this.setProperty('onesignalId', value); } @@ -44,7 +40,6 @@ export class SetAliasOperation extends Operation { get label(): string { return this.getProperty('label'); } - private set label(value: string) { this.setProperty('label', value); } @@ -52,7 +47,6 @@ export class SetAliasOperation extends Operation { get value(): string { return this.getProperty('value'); } - private set value(value: string) { this.setProperty('value', value); } diff --git a/src/core/requestService/CreateUserPayload.ts b/src/core/requestService/CreateUserPayload.ts index e12dea61e..bd582f854 100644 --- a/src/core/requestService/CreateUserPayload.ts +++ b/src/core/requestService/CreateUserPayload.ts @@ -1,10 +1,10 @@ -import { SupportedIdentity } from '../models/IdentityModel'; import { SupportedSubscription } from '../models/SubscriptionModels'; +import { Identity } from '../models/UserData'; import { UserPropertiesModel } from '../models/UserPropertiesModel'; export type CreateUserPayload = { properties?: UserPropertiesModel; - identity?: SupportedIdentity; + identity?: Identity; refresh_device_metadata?: boolean; subscriptions?: SupportedSubscription[]; }; diff --git a/src/core/requestService/RequestService.ts b/src/core/requestService/RequestService.ts index c25c02396..eb229e9c4 100644 --- a/src/core/requestService/RequestService.ts +++ b/src/core/requestService/RequestService.ts @@ -1,22 +1,22 @@ -import OneSignalApiBaseResponse from '../../shared/api/OneSignalApiBaseResponse'; import OneSignalApiBase from '../../shared/api/OneSignalApiBase'; -import { IdentityModel, SupportedIdentity } from '../models/IdentityModel'; +import OneSignalApiBaseResponse from '../../shared/api/OneSignalApiBaseResponse'; +import { + SdkInitError, + SdkInitErrorKind, +} from '../../shared/errors/SdkInitError'; +import { encodeRFC3986URIComponent } from '../../shared/utils/Encoding'; +import OneSignalUtils from '../../shared/utils/OneSignalUtils'; +import { IdentityModel } from '../models/IdentityModel'; +import { RequestMetadata } from '../models/RequestMetadata'; import { FutureSubscriptionModel, SubscriptionModel, } from '../models/SubscriptionModels'; +import UserData, { Identity } from '../models/UserData'; +import { UserPropertiesModel } from '../models/UserPropertiesModel'; import AliasPair from './AliasPair'; -import { UpdateUserPayload } from './UpdateUserPayload'; import { CreateUserPayload } from './CreateUserPayload'; -import { RequestMetadata } from '../models/RequestMetadata'; -import { encodeRFC3986URIComponent } from '../../shared/utils/Encoding'; -import OneSignalUtils from '../../shared/utils/OneSignalUtils'; -import { - SdkInitError, - SdkInitErrorKind, -} from '../../shared/errors/SdkInitError'; -import UserData from '../models/UserData'; -import { UserPropertiesModel } from '../models/UserPropertiesModel'; +import { UpdateUserPayload } from './UpdateUserPayload'; export class RequestService { /* U S E R O P E R A T I O N S */ @@ -140,10 +140,10 @@ export class RequestService { static async addAlias( requestMetadata: RequestMetadata, alias: AliasPair, - identity: SupportedIdentity, + identity: Identity, ) { const { appId } = requestMetadata; - return OneSignalApiBase.patch<{ identity: SupportedIdentity }>( + return OneSignalApiBase.patch<{ identity: Identity }>( `apps/${appId}/users/by/${alias.label}/${alias.id}/identity`, { identity }, requestMetadata.jwtHeader, @@ -178,7 +178,7 @@ export class RequestService { labelToRemove: string, ) { const { appId } = requestMetadata; - return OneSignalApiBase.delete<{ identity: SupportedIdentity }>( + return OneSignalApiBase.delete<{ identity: Identity }>( `apps/${appId}/users/by/${alias.label}/${alias.id}/identity/${labelToRemove}`, requestMetadata.jwtHeader, ); @@ -286,11 +286,11 @@ export class RequestService { static async transferSubscription( requestMetadata: RequestMetadata, subscriptionId: string, - identity: SupportedIdentity, + identity: Identity, retainPreviousOwner: boolean, ) { const { appId } = requestMetadata; - return OneSignalApiBase.patch<{ identity: SupportedIdentity }>( + return OneSignalApiBase.patch<{ identity: Identity }>( `apps/${appId}/subscriptions/${subscriptionId}/owner`, { identity: { ...identity }, diff --git a/src/onesignal/UserDirector.ts b/src/onesignal/UserDirector.ts index bc8be4512..05bcd4ecb 100644 --- a/src/onesignal/UserDirector.ts +++ b/src/onesignal/UserDirector.ts @@ -1,8 +1,7 @@ import { OSModel } from '../core/modelRepo/OSModel'; -import { SupportedIdentity } from '../core/models/IdentityModel'; import { SupportedSubscription } from '../core/models/SubscriptionModels'; import { ModelName, SupportedModel } from '../core/models/SupportedModels'; -import UserData from '../core/models/UserData'; +import UserData, { Identity } from '../core/models/UserData'; import { RequestService } from '../core/requestService/RequestService'; import { isCompleteSubscriptionObject } from '../core/utils/typePredicates'; import Environment from '../shared/helpers/Environment'; @@ -51,10 +50,7 @@ export default class UserDirector { } } - const identityOSModel = new OSModel( - ModelName.Identity, - identity, - ); + const identityOSModel = new OSModel(ModelName.Identity, identity); identityOSModel.setOneSignalId(identity.onesignal_id); OneSignal.coreDirector.add( @@ -65,13 +61,13 @@ export default class UserDirector { await this.copyOneSignalIdPromiseFromIdentityModel(); } - static createUserPropertiesModel(): OSModel { + static createUserPropertiesModel(): OSModel { const properties = { language: Environment.getLanguage(), timezone_id: Intl.DateTimeFormat().resolvedOptions().timeZone, }; - const propertiesOSModel = new OSModel( + const propertiesOSModel = new OSModel( ModelName.Properties, properties, ); diff --git a/src/page/managers/LoginManager.ts b/src/page/managers/LoginManager.ts index 90176778b..721f491fd 100644 --- a/src/page/managers/LoginManager.ts +++ b/src/page/managers/LoginManager.ts @@ -1,7 +1,6 @@ import { OSModel } from '../../core/modelRepo/OSModel'; -import { SupportedIdentity } from '../../core/models/IdentityModel'; import { ModelName, SupportedModel } from '../../core/models/SupportedModels'; -import UserData from '../../core/models/UserData'; +import UserData, { Identity } from '../../core/models/UserData'; import AliasPair from '../../core/requestService/AliasPair'; import { RequestService } from '../../core/requestService/RequestService'; import { isCompleteSubscriptionObject } from '../../core/utils/typePredicates'; @@ -181,7 +180,7 @@ export default class LoginManager { } static setExternalId( - identityOSModel: OSModel, + identityOSModel: OSModel, externalId: string, ): void { logMethodCall('LoginManager.setExternalId', { externalId }); @@ -193,7 +192,7 @@ export default class LoginManager { identityOSModel.set('external_id', externalId, false); } - static isIdentified(identity: SupportedIdentity): boolean { + static isIdentified(identity: Identity): boolean { logMethodCall('LoginManager.isIdentified', { identity }); return identity.external_id !== undefined; @@ -410,7 +409,7 @@ export default class LoginManager { static async transferSubscription( appId: string, pushSubscriptionId: string, - identity: SupportedIdentity, + identity: Identity, ): Promise> { Log.error( '^^^ Handling 409 HTTP response reported by the browser above.' + diff --git a/src/shared/helpers/NetworkUtils.ts b/src/shared/helpers/NetworkUtils.ts index 805cc296c..640a664d0 100644 --- a/src/shared/helpers/NetworkUtils.ts +++ b/src/shared/helpers/NetworkUtils.ts @@ -1,3 +1,5 @@ +// Implements logic similar to Android SDK's NetworkUtils +// Reference: https://github.com/OneSignal/OneSignal-Android-SDK/blob/5.1.31/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/NetworkUtils.kt export const ResponseStatusType = { INVALID: 'INVALID', RETRYABLE: 'RETRYABLE', @@ -9,8 +11,6 @@ export const ResponseStatusType = { type ResponseStatusValue = (typeof ResponseStatusType)[keyof typeof ResponseStatusType]; -export const MAX_NETWORK_REQUEST_ATTEMPT_COUNT = 3; - /** * Determines the response status type based on HTTP status code. * @param statusCode - The HTTP status code.