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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
30 changes: 23 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,10 @@ playground.xcworkspace

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# Sample apps: do NOT commit Pods. Podfile.lock is the source of truth;
# users run `pod install` themselves. See:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
Pods/

# Carthage
#
Expand Down Expand Up @@ -95,3 +91,23 @@ fastlane/test_output

iOSInjectionProject/

# Internal planning + AI-agent scratch space (not for distribution)
docs/plans/
docs/superpowers/
docs/reports/

# Editor / IDE
.vscode/
.idea/
*.swp
*.swo
*~

# macOS metadata
.AppleDouble
.LSOverride
._*

# Claude / agent state
.claude-trace/

41 changes: 41 additions & 0 deletions docs/integration-matrix/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Integration matrix — v7 reference

This directory maps the DevHub iOS code-generation matrix to v7-correct snippets. Each snippet is copied **verbatim** from one of the three sample apps in this repo (`obj-c/`, `swift/basic_app/`, `swiftui/basic_app_swiftui/`), so the docs and runnable code stay in lockstep.

The v7 default spine is [`registerSessionReadyListener`](flags/session-ready-listener.md) — the SDK's official timing gate. Every other flag layers onto it. If you're coming from v6, the [v6 → v7 migration page](migration/v6-to-v7.md) covers why the `NotificationCenter.didBecomeActive → start()` workaround is gone and what the SceneDelegate flag means now.

## Preset permutations

| Preset | Description | Swift snippet | ObjC snippet | Source sample |
|--------|-------------|---------------|--------------|---------------|
| `minimal` | Debug + initialize + delegates + handleLaunchOptions + registerSessionReadyListener + start. No ATT, no CUID, no SceneDelegate. | [`snippets/swift/minimal.swift`](snippets/swift/minimal.swift) | [`snippets/objc/minimal.m`](snippets/objc/minimal.m) | derived from `swift/basic_app/` and `obj-c/` |
| `all-flags-on` | Every matrix flag enabled: Debug + CUID + delegates + handleLaunchOptions + Session-ready listener wrapping ATT + start with completion handler. | [`snippets/swift/all-flags-on.swift`](snippets/swift/all-flags-on.swift) | [`snippets/objc/all-flags-on.m`](snippets/objc/all-flags-on.m) | `swift/basic_app/basic_app/AppDelegate.swift`, `obj-c/obj-c/AppDelegate.m` |
| `scene-delegate` | The SceneDelegate file alone (deep-link routing). Pairs with any AppDelegate preset. | [`snippets/swift/scene-delegate.swift`](snippets/swift/scene-delegate.swift) | [`snippets/objc/scene-delegate.m`](snippets/objc/scene-delegate.m) | `swift/basic_app/basic_app/SceneDelegate.swift`, `obj-c/obj-c/SceneDelegate.m` |
| `att-only` | `minimal` + ATT request inside the session-ready block, before `start()`. | combine [`minimal.swift`](snippets/swift/minimal.swift) with the ATT block from [`all-flags-on.swift`](snippets/swift/all-flags-on.swift) | combine [`minimal.m`](snippets/objc/minimal.m) with the ATT block from [`all-flags-on.m`](snippets/objc/all-flags-on.m) | `swift/basic_app/basic_app/AppDelegate.swift`, `obj-c/obj-c/AppDelegate.m` |
| `cuid-only` | `minimal` + CUID assignment before delegates. | combine [`minimal.swift`](snippets/swift/minimal.swift) with the CUID line from [`all-flags-on.swift`](snippets/swift/all-flags-on.swift) | combine [`minimal.m`](snippets/objc/minimal.m) with the CUID line from [`all-flags-on.m`](snippets/objc/all-flags-on.m) | `swift/basic_app/basic_app/AppDelegate.swift`, `obj-c/obj-c/AppDelegate.m` |

The full 16-cell cross product of the four toggleable flags is not enumerated — only the named presets above. Add or remove the matching block from `all-flags-on` to build any other combination.

## Per-flag pages

| Flag | Page | DevHub label |
|------|------|--------------|
| Debug logs | [flags/debug-logs.md](flags/debug-logs.md) | Debug logs |
| SceneDelegate support | [flags/scene-delegate.md](flags/scene-delegate.md) | SceneDelegate |
| ATT | [flags/att.md](flags/att.md) | ATT |
| Customer User ID | [flags/customer-user-id.md](flags/customer-user-id.md) | Customer User ID (CUID) |
| Session-ready listener | [flags/session-ready-listener.md](flags/session-ready-listener.md) | Session-ready listener (v7 default spine — always on) |

## Migration

- [v6 → v7 SceneDelegate migration](migration/v6-to-v7.md) — why the `didBecomeActive` observer is gone, what the SceneDelegate flag means in v7, and what to do for SwiftUI lifecycle apps.

## Sample app reference

| Sample | AppDelegate | SceneDelegate |
|--------|-------------|---------------|
| obj-c | `obj-c/obj-c/AppDelegate.m` | `obj-c/obj-c/SceneDelegate.m` |
| swift | `swift/basic_app/basic_app/AppDelegate.swift` | `swift/basic_app/basic_app/SceneDelegate.swift` |
| swiftui | `swiftui/basic_app_swiftui/AppDelegate.swift` | n/a — `WindowGroup` owns scene management; deep links handled via `.onContinueUserActivity` + `.onOpenURL` in `swiftui/basic_app_swiftui/Screens/MainView.swift` |

Grep `[Matrix flag]` in any of the AppDelegate files to find the marked region for each toggle.
80 changes: 80 additions & 0 deletions docs/integration-matrix/flags/att.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ATT (App Tracking Transparency)

**Purpose:** Request the user's tracking-authorization decision before `start()` so the SDK can include IDFA in the install/session payload when granted. Required by App Store policy for any SDK that reads IDFA.

## Where it lives

| Sample | File | Line |
|--------|------|------|
| obj-c | `obj-c/obj-c/AppDelegate.m` | 46-65 |
| swift | `swift/basic_app/basic_app/AppDelegate.swift` | 41-60 |
| swiftui | `swiftui/basic_app_swiftui/AppDelegate.swift` | 56-75 |

ATT lives **inside** the `registerSessionReadyListener` block, **before** `start()`. This replaces v6's `waitForATTUserAuthorization:` — v7 has no built-in ATT timeout. The session-ready listener guarantees the SDK is ready, and your code controls when ATT is requested.

## Swift

```swift
// MARK: - [Matrix flag] Session-ready listener (v7 default spine)
AppsFlyerLib.shared().registerSessionReadyListener {
// MARK: - [Matrix flag] ATT
if #available(iOS 14, *) {
ATTrackingManager.requestTrackingAuthorization { _ in
AppsFlyerLib.shared().start { (dictionary, error) in
if let error = error {
NSLog("[AFSDK] start failed: \(error)")
return
}
NSLog("[AFSDK] start succeeded: \(dictionary ?? [:])")
}
}
} else {
AppsFlyerLib.shared().start { (dictionary, error) in
if let error = error {
NSLog("[AFSDK] start failed: \(error)")
return
}
NSLog("[AFSDK] start succeeded: \(dictionary ?? [:])")
}
}
}
```

## Objective-C

```objectivec
#pragma mark - [Matrix flag] Session-ready listener (v7 default spine)
[[AppsFlyerLib shared] registerSessionReadyListener:^{
#pragma mark - [Matrix flag] ATT
// ATT request goes here if the app needs it before start
if (@available(iOS 14, *)) {
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
[[AppsFlyerLib shared] startWithCompletionHandler:^(NSDictionary<NSString *, id> * _Nullable dictionary, NSError * _Nullable error) {
if (error) {
NSLog(@"[AFSDK] start failed: %@", error);
return;
}
NSLog(@"[AFSDK] start succeeded: %@", dictionary);
}];
}];
} else {
[[AppsFlyerLib shared] startWithCompletionHandler:^(NSDictionary<NSString *, id> * _Nullable dictionary, NSError * _Nullable error) {
if (error) {
NSLog(@"[AFSDK] start failed: %@", error);
return;
}
NSLog(@"[AFSDK] start succeeded: %@", dictionary);
}];
}
}];
```

## Behavioral notes

- **Info.plist:** `NSUserTrackingUsageDescription` is required. Without it, the prompt never displays and `requestTrackingAuthorization` resolves with `.denied`.
- **Timing:** the prompt only shows once per install. After the user decides, subsequent calls resolve immediately with the stored status.
- **Threading:** the completion handler fires on an arbitrary queue. Don't touch UIKit there. Calling `start()` from the completion is fine — `AppsFlyerLib` handles its own threading.
- **iOS < 14:** the `#available` / `@available` else branch calls `start()` directly. IDFA is available unconditionally on pre-iOS-14 devices.
- **If omitted:** `start()` runs without IDFA on iOS 14+ devices (treated as ATT-denied by Apple). Attribution still works via probabilistic and SKAdNetwork paths, but install-quality signals degrade.
- **tvOS:** ATT is unavailable. Skip this block entirely on tvOS targets.
- **Don't:** call `start()` outside the `registerSessionReadyListener` block just because ATT denied. The listener is the gate; the ATT branch only decides whether to prompt first.
36 changes: 36 additions & 0 deletions docs/integration-matrix/flags/customer-user-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Customer User ID (CUID)

**Purpose:** Tag every event sent by the SDK with your app's internal user identifier. The CUID flows through to raw-data reports and downstream BI so attribution can be reconciled against your own user model.

## Where it lives

| Sample | File | Line |
|--------|------|------|
| obj-c | `obj-c/obj-c/AppDelegate.m` | 32-33 |
| swift | `swift/basic_app/basic_app/AppDelegate.swift` | 27-28 |
| swiftui | `swiftui/basic_app_swiftui/AppDelegate.swift` | 39-40 |

Set **after** `initialize(...)` and **before** `start()`. Setting it before `start()` ensures the install event itself carries the CUID; setting it after `start()` only tags subsequent events.

## Swift

```swift
// MARK: - [Matrix flag] Customer User ID (CUID)
AppsFlyerLib.shared().customerUserID = "my user id"
```

## Objective-C

```objectivec
#pragma mark - [Matrix flag] Customer User ID (CUID)
[AppsFlyerLib shared].customerUserID = @"my user id";
```

## Behavioral notes

- **Persistence:** the SDK stores the CUID across launches. Set it once; it sticks until you change it or call `setCustomerUserID(nil)`.
- **Timing:** set before `start()` to include it on the install event. If your user identifier is only known after login, set it then — the install will be re-attributed once it arrives, but only via downstream reconciliation, not on the original install row.
- **Threading:** simple property setter, safe from any thread. Conventionally main.
- **Privacy:** the CUID is yours — don't use raw PII (email, phone) as the value. Hash if needed. Most apps use a backend-issued opaque ID.
- **If omitted:** events ship without `customer_user_id`. Attribution still works; you just can't join reports back to your own user table by this column.
- **Anonymize interaction:** if `anonymizeUser = true`, the CUID is still sent. `anonymizeUser` suppresses device identifiers, not your CUID.
36 changes: 36 additions & 0 deletions docs/integration-matrix/flags/debug-logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Debug logs

**Purpose:** Enable verbose SDK logging during development. Logs install, session, attribution, deep-link, and network events to the Xcode console.

## Where it lives

| Sample | File | Line |
|--------|------|------|
| obj-c | `obj-c/obj-c/AppDelegate.m` | 25-27 |
| swift | `swift/basic_app/basic_app/AppDelegate.swift` | 21-22 |
| swiftui | `swiftui/basic_app_swiftui/AppDelegate.swift` | 33-34 |

Set this **before** anything else on `AppsFlyerLib.shared()`. It costs nothing to set early, and a few setters log at construction time.

## Swift

```swift
// MARK: - [Matrix flag] Debug logs
AppsFlyerLib.shared().isDebug = true
```

## Objective-C

```objectivec
#pragma mark - [Matrix flag] Debug logs
// Set isDebug to true to see AppsFlyer debug logs
[AppsFlyerLib shared].isDebug = YES;
```

## Behavioral notes

- **Default:** `false`. The SDK ships silent.
- **Timing:** safe at any point, but earlier is better — late toggles miss bootstrap-time logs.
- **Threading:** main thread is conventional; the property is a simple BOOL set.
- **If omitted:** the SDK still runs normally. You just get no console output. Set this when reproducing an attribution issue or wiring up a new integration.
- **Production:** ship with `isDebug = false`. Leaving it on inflates console noise and can leak request/response payloads to anyone with a device log.
88 changes: 88 additions & 0 deletions docs/integration-matrix/flags/scene-delegate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# SceneDelegate

**Purpose:** Route Universal Links (`NSUserActivity`) and URI schemes (`UIOpenURLContext`) into the SDK in scene-based apps. In v7, this flag generates the `SceneDelegate` file **only**. It does **not** add a `NotificationCenter.didBecomeActive` observer — see [migration/v6-to-v7.md](../migration/v6-to-v7.md).

## Where it lives

| Sample | File | Line |
|--------|------|------|
| obj-c | `obj-c/obj-c/SceneDelegate.m` | 15-39 |
| swift | `swift/basic_app/basic_app/SceneDelegate.swift` | 12-37 |
| swiftui | n/a — `App` lifecycle apps don't get a SceneDelegate; use `.onContinueUserActivity` + `.onOpenURL` view modifiers. See `swiftui/basic_app_swiftui/Screens/MainView.swift:71-78`. |

## Swift

```swift
// MARK: - [Matrix flag] SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?

func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
if let userActivity = connectionOptions.userActivities.first {
AppsFlyerLib.shared().continue(userActivity, restorationHandler: nil)
}
for urlContext in connectionOptions.urlContexts {
AppsFlyerLib.shared().handleOpen(urlContext.url, options: nil)
}
}

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
AppsFlyerLib.shared().continue(userActivity, restorationHandler: nil)
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for context in URLContexts {
AppsFlyerLib.shared().handleOpen(context.url, options: nil)
}
}
}
```

## Objective-C

```objectivec
#pragma mark - [Matrix flag] SceneDelegate
@implementation SceneDelegate


- (void)scene:(UIScene *)scene
willConnectToSession:(UISceneSession *)session
options:(UISceneConnectionOptions *)connectionOptions {
NSUserActivity *userActivity = connectionOptions.userActivities.anyObject;
if (userActivity) {
[[AppsFlyerLib shared] continueUserActivity:userActivity restorationHandler:nil];
}
for (UIOpenURLContext *urlContext in connectionOptions.URLContexts) {
[[AppsFlyerLib shared] handleOpenUrl:urlContext.URL options:nil];
}
}

- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
[[AppsFlyerLib shared] continueUserActivity:userActivity restorationHandler:nil];
}

- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
for (UIOpenURLContext *urlContext in URLContexts) {
[[AppsFlyerLib shared] handleOpenUrl:urlContext.URL options:nil];
}
}
```

## Behavioral notes

- **Three delivery channels:**
- `scene(_:willConnectTo:options:)` — cold launch via deep link. The scene didn't exist yet; both Universal Links and URI schemes arrive in `connectionOptions`.
- `scene(_:continue:)` — Universal Link delivered to an already-connected scene (warm path).
- `scene(_:openURLContexts:)` — URI scheme delivered to an already-connected scene.
- **Symbol naming (ObjC):** the SDK exposes `handleOpenUrl:options:` (lowercase `u`). Do not write `handleOpenURL:options:`.
- **Timing:** these methods can fire before, during, or after `registerSessionReadyListener` resolves. The SDK buffers deep-link inputs internally and replays them once readiness is reached.
- **Threading:** UIKit calls these on the main thread.
- **If omitted in a scene-based app:** Universal Links and URI schemes silently drop. `AppDelegate`'s `continueUserActivity` / `openURL:` overrides are bypassed by UIKit once a SceneDelegate is wired up in `Info.plist`.
- **SwiftUI lifecycle:** see the migration callout — `WindowGroup` owns scene management, so the SDK is fed via `.onContinueUserActivity` and `.onOpenURL` instead.

## Migration

See [migration/v6-to-v7.md](../migration/v6-to-v7.md) for why the v6 `NotificationCenter.didBecomeActive` workaround is gone and what the flag means now.
Loading