Feat/multiinstance ios - #218
Merged
Merged
Conversation
Add a process-wide SdkRegistry that tracks PersonalizationSDK instances by shop_id, alongside pure InstanceResolver / PushTargetResolver decision logic and a Cancellable subscription handle. createPersonalizationSDK now registers each instance; deleteUserCredentials and a new deinit unregister it. Instances are held weakly, so a host-released SDK drops out of the registry on deallocation. No public API is added and single-instance behaviour is unchanged. Cover the registry and both resolvers with unit tests in REES46Tests (28 tests), including the weak-reference auto-drop.
…itory Route device_id / seance / IDFA reads and writes through a new UserIdentityRepository port (UserDefaults-backed) instead of poking UserDefaults.standard directly across SimplePersonalizationSDK. Each shop gets its own UserDefaults suite (personalization_sdk_<shopId>), so two shops in one app no longer share identity. The push token stays device-global. A one-time migration adopts the pre-partition identity from the shared domain into the first shop's partition (consume-once), so an existing install keeps its did with no re-registration; a host that supplies a custom storage key is used verbatim with no migration. Cover partition naming, the migration guard, the repository, and multi-instance isolation with unit tests.
Key the keychain backup of the init secret by shop (account = "<bundleId>.<shopId>") instead of the bundle id alone, so two shops in one app no longer share the item and a reinstall restores each shop's own did. A one-time adoption copies the pre-partition item into the first shop's slot and then removes it, so an existing install keeps its did across a reinstall without a second shop cloning it. Route keychain read/write through a KeychainInitStore port, and clear it in deleteUserCredentials (previously the backup outlived a credentials wipe and resurrected the deleted did). Cover the account naming and the store (round-trip, two-shop isolation, legacy adoption) with unit tests.
Resolve the session id through a new SessionManager (injectable Clock and id generator): reuse the persisted seance while last activity is within 2 hours (the window rolls forward), otherwise start a fresh 10-character alphanumeric one. The activity anchor (seanceLastActTime) lives in the per-shop identity partition, so the session survives cold starts and stays isolated per shop — aligning iOS with the Android and React Native SDKs. The session is now client-owned: the server-assigned seance is no longer adopted, so continuity no longer depends on the backend echoing it. The device id is unchanged. Cover reuse / expiry / rolling refresh / generation and the timestamp round-trip with unit tests.
…aking upgrade Route viewed slides, downloaded media, cart and favourites through a per-shop LocalStateRepository instead of the shared UserDefaults.standard, so two shops in one app no longer share or overwrite each other's stories state. shopId is threaded through StoryContent -> Story -> Slide (slide ids are not unique across shops), and the reset* methods delegate to the repository, so constructing one shop no longer wipes another shop's viewed state. Add an SDK-level upgrade test that seeds a pre-partition install's identity in the real .standard domain and keychain and drives it through the production UserIdentityRepositoryImpl(shopId:) / KeychainInitStoreImpl(shopId:service:) constructors, proving an existing install keeps its did/seance/IDFA/keychain blob (no silent re-registration) and a second shop never clones it.
Add the unified entry point — Rees46.initialize / register(shops:) / instance(for:) / isInitialized / awaitInstance — backed by Rees46Config and a Swift Rees46Error enum (unknownShopId / ambiguousShop). Resolution reuses the internal registry and InstanceResolver; shops can be registered lazily and materialize on first use. Extract makeRegisteredSDK as the single construction path (build, reset the per-shop cache, register) shared by the facade and createPersonalizationSDK, and thread an optional storageKey through it. Deprecate createPersonalizationSDK in favour of the facade — it still works and returns the same type. Add a StoriesView.configure(shopId:) overload that resolves its instance reactively via awaitInstance, and make Cancellable public so the facade can hand it back. Cover the facade with Rees46FacadeTests: resolution, the error contract, lazy registration, and await/cancel.
Add Rees46.handlePush(_:event:): resolve the target shop from the payload's shop_id via PushTargetResolver, then track received/clicked against that instance. A push for an unknown shop — or with no shop_id while several shops are live — is dropped rather than tracked against the wrong one; a single-instance app still resolves. Navigation stays with the host; handlePush only tracks. Add PushEvent (received/clicked) and a pure PushPayloadParser for shop_id and (type, code); NotificationService.extractTypeAndCode now delegates to it. Make the push-token upload throttle per shop (mainPushTokenLastUploadDateKey .<shopId>) so one shop's upload no longer throttles the others — each initialized instance registers the device token with its own backend. Cover routing and parsing with HandlePushRoutingTests and PushPayloadParserTests.
NotificationService tracked delivered/received/clicked against the single sdk it captured at init, so in a multi-shop app every push was attributed to the wrong shop. Route all three through Rees46.handlePush, which resolves the target by the payload's shop_id; navigation, token registration and notification categories stay in the service. Add PushEvent.delivered so the delivery beacon is routed too. The service no longer drives notificationTrackerDelegate (handlePush does the tracking now, so calling it would double-track); NotificationTracker is kept for source compatibility but is inert. Single-shop behaviour is unchanged — handlePush resolves the one live shop, the same instance the service captured. Extend HandlePushRoutingTests with the delivered case.
Add MultiInstanceViewController: two shops live in one app, each resolved by shopId through Rees46.instance(for:) with no globalSDK — session cards, fail-fast contract buttons, push-routing buttons, per-shop stories via the StoriesView shopId overload, and a log. Reachable from a button on the main screen, whose DID label now reads getDeviceId() instead of the pre-partition UserDefaults.standard key. Add MultiInstanceE2ETest against the live backend: two shops resolve to distinct instances, the default is ambiguous, an unknown shop throws, and the two shops keep isolated sessions (distinct client seances; the did is device-level). Point the tests at the real test shop — the previous default was deactivated and the backend returns "shop not found" for it — and add a second real shop. Update DeviceIdSaveTest to assert the did persists in the shop partition, not the shared UserDefaults.standard key.
- Rees46.handlePush: resolve via InstanceResolver (live + pending) and track a registered-but-not-initialized shop's push standalone (PushDeliveryTracker) instead of dropping it - remove the now-unused PushTargetResolver and its tests - SdkRegistry: fix a stale routing comment - tests: pending-push routing, upgrade migration
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.