Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- Added `IterableAPI.switchProject(project:callback:)` for apps that need to move a running install from one Iterable project to another (for example a multi-region app moving between a US and an EU project) without an app restart. The method returns immediately and runs the whole sequence off the calling thread: it disables the push token on the previous project, resets that project's in-app and embedded state, purges its persisted offline queue, clears its identity from memory and from storage (email, user ID, unknown-user ID and auth token) along with the rest of that project's storage (activation criteria, unsent unknown-user events, sessions and updates, and stored attribution info), and then stands up a fresh instance against the new API key and config. SDK calls made between the call and the callback are queued and then run in order against the new project.
- The callback is delivered on the main thread and receives an `IterableProjectSwitchResult`, either `.switchedCleanly` or `.switchedWithWarnings`. Both mean the SDK **is** on the new project; they differ only in whether leaving the previous one was tidy, and neither means the switch failed or was rolled back. A named result rather than a boolean because the meaning of a bare `false` had to be explained everywhere it appeared. The SDK reports `.switchedWithWarnings` when push registration is off, when there is no device token to disable, when no user was identified on the previous project, or when the device disable request fails, so for an app that does not use push it is the expected outcome and not an error: carry on and re-identify the user exactly as you would after `.switchedCleanly`. The callback is not held open for the device disable response, so a disable that fails after the callback has been delivered is logged rather than reported retroactively.
- `switchProject` does not re-identify the user. Call `setEmail`/`setUserId` from the callback. The JWT auth retry budget does not carry over: it is state on the `AuthManager` instance, and the switch builds a fresh one along with the rest of the dependency container.
- Added `IterableProject`, which pairs a project's API key with the `IterableConfig` to run it with. `switchProject` takes one of these rather than a loose key and config, because `IterableConfig` carries the data region and the `authDelegate`: supplying the two halves separately allows one project's key to be paired with another project's region or auth delegate, and that mismatch produces an authentication failure, or a request sent to the wrong region, that looks unrelated to the project it came from. Its initializer is failable and returns `nil` for an empty or whitespace-only key, so an unusable project cannot be built and a blank key can no longer reach a teardown. Note that `IterableConfig` holds its delegates weakly, so an app keeping a project as a long-lived constant should keep a strong reference to its delegates too.
- Called before any `initialize`, it behaves as `initialize` and logs a warning, and reports `.switchedCleanly` once that initialization has started: there is no previous project, so no teardown step could have been noisy. Android reports the same.
- Repeated calls are resolved against the project most recently *asked* for rather than the one currently live, so the SDK always ends up where the app last asked to be. A request for the project already asked for is absorbed: with nothing in flight it is a no-op reporting `.switchedCleanly`, and while a switch to it is running its callback joins that switch rather than starting a second teardown, so a picker tapped twice on one destination runs a single teardown. Anything else is queued and runs in the order it was asked for, each callback firing when the project it asked for is live. Asking to go back to the project a running switch is leaving is therefore a real switch and not a no-op, and `A β†’ B β†’ A` reports B and then A and ends on A. Between two switches in a chain the SDK is fully live on the project that just landed, so calls made from its callback, re-identifying the user in particular, run against that project instead of being replayed into the next one.
- Queued `users/disableDevice` requests survive the purge, so the token is still disabled on the project being left even when the switch happens offline. The device ID is project-agnostic and is preserved. The switch waits briefly, and with a bound, for that disable to reach the request layer before it releases the previous project's instance, because a request built with the previous API key cannot be sent once that instance is gone; it does not wait for the network response.
- Calls that carry the previous project's campaign, template or message IDs are **not** queued: push handling, in-app and embedded tracking, `inAppConsume`, `updateSubscriptions` and inbox sessions run against the project the SDK is on when they are made. Neither are calls where deferral is itself the problem: device disables, visitor consent, device attributes and `pauseAuthRetries`. Identity and generic tracking (`setEmail`, `setUserId`, `updateEmail`, `updateUser`, `updateCart`, `trackPurchase`, `track`, `registerToken`, `logoutUser`) are queued and replayed.

### Fixed
- `switchProject` called for a second project while a switch is still running no longer discards that request. The SDK used to finish on the first project while the second caller's callback still reported a completed switch, so an app whose region or brand picker was tapped twice ended up sending every later event to the project the user had already moved away from, with nothing in the API to detect it. The second request is now run as soon as the first finishes, on the chain the first hands to it rather than a fresh one, so a third request made in between, from the first switch's callback for instance, cannot claim the chain first and then be overtaken by the queued one. Requests are resolved against the project asked for most recently, so repeated taps on one destination still share a single teardown, a repeat that crosses another destination runs again in its turn rather than being folded into the earlier one, and asking to go back to the project a running switch is leaving is honoured instead of being mistaken for a no-op and reported as a clean switch that never happened.
- Calls queued during a project switch can no longer be overtaken by a call that arrives as the switch lands. The gate was lowered before the queue was replayed, so a `setEmail` made at that moment could run first and then be overwritten by the older one replayed behind it. The gate now stays raised until the replay is finished.
- Identity set from a `switchProject` callback now reaches the project that just landed, even when another switch is already queued behind it. The queued switch inherited a still-raised gate, so a `setEmail` made from the callback, which is where the contract says to make it, was queued and then replayed against the next project in the chain, identifying one project's user on another. The gate that queues calls is now released as each switch lands, and the ordering of queued switch requests is tracked separately from it.
- The check that stops a deep link redirect from writing the previous project's attribution back after a switch is now performed under a lock together with the write. Reading the flag and then writing outside it left a window in which a redirect could pass the check, have the switch clear attribution, and then put the previous project's campaign back into the storage the new project reads.

## [6.7.5]
### Added
Expand Down
24 changes: 24 additions & 0 deletions swift-sdk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@
55B37FEE229F59290042F13A /* InAppPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B37FED229F59290042F13A /* InAppPersistenceTests.swift */; };
55B5498423973B5C00243E87 /* InboxSessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B5498323973B5C00243E87 /* InboxSessionManagerTests.swift */; };
55B549862397462300243E87 /* InboxImpressionTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B549852397462300243E87 /* InboxImpressionTrackerTests.swift */; };
5D6740010000000000000004 /* SwitchProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6740010000000000000003 /* SwitchProjectTests.swift */; };
5D6740010000000000000008 /* SwitchProjectGateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6740010000000000000007 /* SwitchProjectGateTests.swift */; };
55B9F15124B3D33700E8198A /* AuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B9F15024B3D33700E8198A /* AuthTests.swift */; };
55CC257B2462064F00A77FD5 /* InAppPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CC257A2462064F00A77FD5 /* InAppPresenterTests.swift */; };
55E02D39253F8D86009DB8BC /* WebViewProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E02D38253F8D86009DB8BC /* WebViewProtocolTests.swift */; };
Expand All @@ -186,6 +188,9 @@
8AAA8BB52D07310600DF8220 /* IterableEmbeddedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8B222D07310600DF8220 /* IterableEmbeddedMessage.swift */; };
8AAA8BB82D07310600DF8220 /* IterableMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8B9C2D07310600DF8220 /* IterableMessaging.swift */; };
8AAA8BC32D07310600DF8220 /* IterableInboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8BA52D07310600DF8220 /* IterableInboxViewController.swift */; };
5D6740010000000000000002 /* IterableAPI+SwitchProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6740010000000000000001 /* IterableAPI+SwitchProject.swift */; };
5D6740020000000000000002 /* IterableProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6740020000000000000001 /* IterableProject.swift */; };
5D6740030000000000000002 /* IterableProjectSwitchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6740030000000000000001 /* IterableProjectSwitchResult.swift */; };
8AAA8BC42D07310600DF8220 /* IterableAppIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8B992D07310600DF8220 /* IterableAppIntegration.swift */; };
8AAA8BCD2D07310600DF8220 /* RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8B252D07310600DF8220 /* RetryPolicy.swift */; };
9313470310D6B94EBF01743D /* AuthTokenValidityState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3BD1417B5702B16498AFF /* AuthTokenValidityState.swift */; };
Expand Down Expand Up @@ -330,6 +335,7 @@
AC2C668720D3435700D46CC9 /* ActionRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2C668620D3435700D46CC9 /* ActionRunnerTests.swift */; };
AC347B6720E699FA003449CF /* IterableAppExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = AC347B6620E699D8003449CF /* IterableAppExtensions.h */; settings = {ATTRIBUTES = (Public, ); }; };
AC3A2FF0262EDD4C00425435 /* InAppPriorityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3A2FEF262EDD4C00425435 /* InAppPriorityTests.swift */; };
5D6740010000000000000006 /* SwitchProjectOfflineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D6740010000000000000005 /* SwitchProjectOfflineTests.swift */; };
AC3EFFF02510B8FB007F1330 /* TaskSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3EFFEF2510B8FB007F1330 /* TaskSchedulerTests.swift */; };
AC43E7FF23A8C2C0008917E2 /* image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = AC74FE1E23A8C0DB004AC442 /* image.jpg */; };
AC4BA00224163D8F007359F1 /* IterableHtmlMessageViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC4BA00124163D8F007359F1 /* IterableHtmlMessageViewControllerTests.swift */; };
Expand Down Expand Up @@ -631,6 +637,8 @@
55B37FED229F59290042F13A /* InAppPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPersistenceTests.swift; sourceTree = "<group>"; };
55B5498323973B5C00243E87 /* InboxSessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxSessionManagerTests.swift; sourceTree = "<group>"; };
55B549852397462300243E87 /* InboxImpressionTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxImpressionTrackerTests.swift; sourceTree = "<group>"; };
5D6740010000000000000003 /* SwitchProjectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwitchProjectTests.swift"; sourceTree = "<group>"; };
5D6740010000000000000007 /* SwitchProjectGateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwitchProjectGateTests.swift"; sourceTree = "<group>"; };
55B9F15024B3D33700E8198A /* AuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTests.swift; sourceTree = "<group>"; };
55CC257A2462064F00A77FD5 /* InAppPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPresenterTests.swift; sourceTree = "<group>"; };
55E02D38253F8D86009DB8BC /* WebViewProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProtocolTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -660,6 +668,9 @@
8AAA8B302D07310600DF8220 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
8AAA8B6C2D07310600DF8220 /* IterableSDK.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IterableSDK.h; sourceTree = "<group>"; };
8AAA8B982D07310600DF8220 /* IterableAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAPI.swift; sourceTree = "<group>"; };
5D6740010000000000000001 /* IterableAPI+SwitchProject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IterableAPI+SwitchProject.swift"; sourceTree = "<group>"; };
5D6740020000000000000001 /* IterableProject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableProject.swift; sourceTree = "<group>"; };
5D6740030000000000000001 /* IterableProjectSwitchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableProjectSwitchResult.swift; sourceTree = "<group>"; };
8AAA8B992D07310600DF8220 /* IterableAppIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAppIntegration.swift; sourceTree = "<group>"; };
8AAA8B9A2D07310600DF8220 /* IterableConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableConfig.swift; sourceTree = "<group>"; };
8AAA8B9B2D07310600DF8220 /* IterableLogging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableLogging.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -787,6 +798,7 @@
AC2C668620D3435700D46CC9 /* ActionRunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRunnerTests.swift; sourceTree = "<group>"; };
AC347B6620E699D8003449CF /* IterableAppExtensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IterableAppExtensions.h; sourceTree = "<group>"; };
AC3A2FEF262EDD4C00425435 /* InAppPriorityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppPriorityTests.swift; sourceTree = "<group>"; };
5D6740010000000000000005 /* SwitchProjectOfflineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwitchProjectOfflineTests.swift"; sourceTree = "<group>"; };
AC3EFFEF2510B8FB007F1330 /* TaskSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskSchedulerTests.swift; sourceTree = "<group>"; };
AC4BA00124163D8F007359F1 /* IterableHtmlMessageViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableHtmlMessageViewControllerTests.swift; sourceTree = "<group>"; };
AC50865324C60172001DC132 /* IterableDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = IterableDataModel.xcdatamodel; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1099,6 +1111,9 @@
isa = PBXGroup;
children = (
8AAA8B982D07310600DF8220 /* IterableAPI.swift */,
5D6740010000000000000001 /* IterableAPI+SwitchProject.swift */,
5D6740020000000000000001 /* IterableProject.swift */,
5D6740030000000000000001 /* IterableProjectSwitchResult.swift */,
8AAA8B992D07310600DF8220 /* IterableAppIntegration.swift */,
8AAA8B9A2D07310600DF8220 /* IterableConfig.swift */,
8AAA8B9B2D07310600DF8220 /* IterableLogging.swift */,
Expand Down Expand Up @@ -1495,6 +1510,8 @@
AC3A3075262EE26D00425435 /* request-tests */,
00B6FACF210E8B10007535CF /* Test Files */,
AC2C668620D3435700D46CC9 /* ActionRunnerTests.swift */,
5D6740010000000000000003 /* SwitchProjectTests.swift */,
5D6740010000000000000007 /* SwitchProjectGateTests.swift */,
55B9F15024B3D33700E8198A /* AuthTests.swift */,
ACB37AAF240268A60093A8EA /* SampleInboxViewDelegateImplementations.swift */,
8A272FD12DD3775900634559 /* unit-tests-Bridging-Header.h */,
Expand Down Expand Up @@ -1684,6 +1701,7 @@
ACCF274B24F40C85004862D5 /* RequestHandlerTests.swift */,
ACC362C424D2C190002C67BA /* TaskProcessorTests.swift */,
AC2AED4124EBC60C000EE5F3 /* TaskRunnerTests.swift */,
5D6740010000000000000005 /* SwitchProjectOfflineTests.swift */,
AC3EFFEF2510B8FB007F1330 /* TaskSchedulerTests.swift */,
ACFD5AC524C8216A008E497A /* TasksCRUDTests.swift */,
ACB1DFD026369CC300A31597 /* HealthMonitorTests.swift */,
Expand Down Expand Up @@ -2222,6 +2240,9 @@
8AAA8BB82D07310600DF8220 /* IterableMessaging.swift in Sources */,
8AB8D7D22D3805A900DECFE5 /* IterableAPIMobileFrameworkDetector.swift in Sources */,
8AAA8BC32D07310600DF8220 /* IterableInboxViewController.swift in Sources */,
5D6740010000000000000002 /* IterableAPI+SwitchProject.swift in Sources */,
5D6740020000000000000002 /* IterableProject.swift in Sources */,
5D6740030000000000000002 /* IterableProjectSwitchResult.swift in Sources */,
8AAA8BC42D07310600DF8220 /* IterableAppIntegration.swift in Sources */,
8AAA8BCD2D07310600DF8220 /* RetryPolicy.swift in Sources */,
9313470310D6B94EBF01743D /* AuthTokenValidityState.swift in Sources */,
Expand Down Expand Up @@ -2411,6 +2432,8 @@
5588DF8128C04494000697D7 /* MockUrlDelegate.swift in Sources */,
5585DF8F22A73390000A32B9 /* IterableInboxViewControllerTests.swift in Sources */,
18A3520C2C85BAF0007FED53 /* IsOneOfInNotOneOfCriteareaTest.swift in Sources */,
5D6740010000000000000004 /* SwitchProjectTests.swift in Sources */,
5D6740010000000000000008 /* SwitchProjectGateTests.swift in Sources */,
55B9F15124B3D33700E8198A /* AuthTests.swift in Sources */,
55B06F3829D5102800C3B1BC /* BlankApiClient.swift in Sources */,
5588DFE928C046D7000697D7 /* MockInboxState.swift in Sources */,
Expand Down Expand Up @@ -2624,6 +2647,7 @@
5588DFBC28C045E3000697D7 /* MockInAppDelegate.swift in Sources */,
5588DFB428C045C9000697D7 /* MockInAppDisplayer.swift in Sources */,
5588DF8C28C044BE000697D7 /* MockCustomActionDelegate.swift in Sources */,
5D6740010000000000000006 /* SwitchProjectOfflineTests.swift in Sources */,
AC3EFFF02510B8FB007F1330 /* TaskSchedulerTests.swift in Sources */,
5588DFAC28C045AE000697D7 /* MockInAppFetcher.swift in Sources */,
AC67AF982507481200C1E974 /* NetworkConnectivityCheckerTests.swift in Sources */,
Expand Down
Loading
Loading