diff --git a/.github/actions/setup-demo/action.yml b/.github/actions/setup-demo/action.yml
new file mode 100644
index 000000000..b796ad7a1
--- /dev/null
+++ b/.github/actions/setup-demo/action.yml
@@ -0,0 +1,35 @@
+name: 'Setup Demo'
+description: 'Installs XcodeGen, generates the demo Xcode project, and creates Secrets.plist for Appium E2E'
+inputs:
+ onesignal-app-id:
+ description: 'OneSignal App ID for the demo Secrets.plist'
+ required: true
+ onesignal-api-key:
+ description: 'OneSignal API Key for the demo Secrets.plist'
+ required: true
+runs:
+ using: 'composite'
+ steps:
+ - name: Install XcodeGen
+ shell: bash
+ run: brew install xcodegen
+
+ - name: Create Secrets.plist
+ shell: bash
+ working-directory: examples/demo
+ env:
+ APP_ID: ${{ inputs.onesignal-app-id }}
+ API_KEY: ${{ inputs.onesignal-api-key }}
+ run: |
+ python3 - <<'PY'
+ import os, plistlib, pathlib
+ pathlib.Path('App/Secrets.plist').write_bytes(plistlib.dumps({
+ 'ONESIGNAL_APP_ID': os.environ['APP_ID'],
+ 'ONESIGNAL_API_KEY': os.environ['API_KEY'],
+ }))
+ PY
+
+ - name: Generate Xcode project
+ shell: bash
+ working-directory: examples/demo
+ run: xcodegen generate
diff --git a/.github/os_probot_metadata.js b/.github/os_probot_metadata.js
deleted file mode 100644
index 9708a5922..000000000
--- a/.github/os_probot_metadata.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * Based on probot-metadata - https://github.com/probot/metadata
- */
-const regex = /\n\n/
-
-const { Octokit } = require("@octokit/action")
-
-const octokit = new Octokit()
-
-module.exports = (context, issue = null) => {
- console.log(context)
- const prefix = "onesignal-probot"
-
- if (!issue) issue = context.payload.issue
-
- return {
- async get (key = null) {
- let body = issue.body
-
- if (!body) {
- body = (await octokit.issues.get(issue)).data.body || ''
- }
-
- const match = body.match(regex)
-
- if (match) {
- const data = JSON.parse(match[1])[prefix]
- return key ? data && data[key] : data
- }
- },
-
- async set (key, value) {
- let body = issue.body
- let data = {}
-
- if (!body) body = (await octokit.issues.get(issue)).data.body || ''
-
- body = body.replace(regex, (_, json) => {
- data = JSON.parse(json)
- return ''
- })
-
- if (!data[prefix]) data[prefix] = {}
-
- if (typeof key === 'object') {
- Object.assign(data[prefix], key)
- } else {
- data[prefix][key] = value
- }
-
- body = `${body}\n\n`
-
- const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/")
- const issue_number = context.payload.issue.number
- return octokit.issues.update({ owner, repo, issue_number, body })
- }
- }
-}
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
deleted file mode 100644
index 48f81b5af..000000000
--- a/.github/release-drafter.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name-template: $RESOLVED_VERSION
-tag-template: $RESOLVED_VERSION
-categories:
- - title: 🚀 Features
- label: Enhancement / Feature
- - title: 🐛 Bug Fixes
- label: Bug
- - title: 🧰 Improvements
- label: Improvement
-change-template: '- $TITLE (#$NUMBER)'
-version-resolver:
- major:
- labels:
- - 'major'
- minor:
- labels:
- - 'minor'
- patch:
- labels:
- - 'patch'
- default: patch
-template: |
- ## Other Changes
-
- $CHANGES
\ No newline at end of file
diff --git a/.github/set_response_times.js b/.github/set_response_times.js
deleted file mode 100644
index 5bcac4492..000000000
--- a/.github/set_response_times.js
+++ /dev/null
@@ -1,47 +0,0 @@
-function calcResponseTimeForIssueCreatedAt(createdAt) {
- const issueOpenedDate = new Date(createdAt);
- const issueTriagedDate = new Date();
- const businessDaysResponseTime = calcBusinessDaysBetweenDates(issueOpenedDate, issueTriagedDate);
- return businessDaysResponseTime;
-}
-
-function calcBusinessDaysBetweenDates(openedDate, triagedDate) {
- let differenceInWeeks, responseTime;
- if (triagedDate < openedDate)
- return -1; // error code if dates transposed
- let openedDay = openedDate.getDay(); // day of week
- let triagedDay = triagedDate.getDay();
- openedDay = (openedDay == 0) ? 7 : openedDay; // change Sunday from 0 to 7
- triagedDay = (triagedDay == 0) ? 7 : triagedDay;
- openedDay = (openedDay > 5) ? 5 : openedDay; // only count weekdays
- triagedDay = (triagedDay > 5) ? 5 : triagedDay;
- // calculate differnece in weeks (1000mS * 60sec * 60min * 24hrs * 7 days = 604800000)
- differenceInWeeks = Math.floor((triagedDate.getTime() - openedDate.getTime()) / 604800000);
- if (openedDay < triagedDay) { //Equal to makes it reduce 5 days
- responseTime = (differenceInWeeks * 5) + (triagedDay - openedDay);
- }
- else if (openedDay == triagedDay) {
- responseTime = differenceInWeeks * 5;
- }
- else {
- responseTime = ((differenceInWeeks + 1) * 5) - (openedDay - triagedDay);
- }
- return (responseTime);
-}
-
-module.exports = async(context, osmetadata) => {
- const foundResponseTime = await osmetadata(context).get('response_time_in_business_days');
- if (foundResponseTime) {
- const foundString = "already found response time in business days: " + foundResponseTime
- console.log(foundString);
- return foundString;
- }
- if (context.payload.comment && context.payload.comment.author_association != "MEMBER" && context.payload.comment.author_association != "OWNER" && context.payload.comment.author_association != "CONTRIBUTOR") {
- return;
- }
- const businessDaysResponseTime = calcResponseTimeForIssueCreatedAt(context.payload.issue.created_at);
- console.log("response time in business days: " + businessDaysResponseTime);
- const result = osmetadata(context, context.payload.issue).set('response_time_in_business_days', businessDaysResponseTime)
- console.log("osmetadata update result: " + result);
- return "set response time in business days: " + businessDaysResponseTime;
-}
diff --git a/.github/workflows/Zapier.yml b/.github/workflows/Zapier.yml
deleted file mode 100644
index 3665dd586..000000000
--- a/.github/workflows/Zapier.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-# This is an action to close asana tasks that were generated by Github issues
-
-name: Zapier web hook
-
-# Controls when the workflow will run
-on:
- # Triggers the workflow on push or pull request events but only for the "main" branch
- issues:
- types: [closed]
-
-permissions:
- issues: read
-
-# A workflow run is made up of one or more jobs that can run sequentially or in parallel
-jobs:
- # This workflow contains a single job called "build"
- build:
- # The type of runner that the job will run on
- runs-on: ubuntu-latest
-
- # Steps represent a sequence of tasks that will be executed as part of the job
- steps:
- # Runs a set of commands using the runners shell
- - name: Call Zapier web hook to close Asana task
- if: ${{ !github.event.issue.pull_request }}
- env:
- ISSUE_TITLE: ${{ github.event.issue.title }}
- run: |
- curl --location --request POST 'https://hooks.zapier.com/hooks/catch/12728683/b7009qc/' \
- --header 'Content-Type: application/json' \
- --header 'Accept: application/json' \
- --data-raw '{
- "task_name" : "$ISSUE_TITLE"
- }'
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
deleted file mode 100644
index 7b9792e19..000000000
--- a/.github/workflows/cd.yml
+++ /dev/null
@@ -1,100 +0,0 @@
-name: iOS CD
-
-on:
- workflow_dispatch:
- inputs:
- version:
- type: string
- description: "The version number of the release"
- required: true
- release_branch:
- type: string
- description: "The release branch with bumped version numbers for the release"
- required: true
-
-jobs:
- build:
- name: Build the binaries for the release and create a PR
- runs-on: macos-13
-
- steps:
- - name: setup xcode
- uses: maxim-lobanov/setup-xcode@v1
- with:
- xcode-version: '15.2'
- - name: Checkout OneSignal-iOS-SDK
- uses: actions/checkout@v4
- with:
- ref: ${{github.event.inputs.release_branch}}
-
- - name: Install the Apple distribution certificate and provisioning profile
- uses: apple-actions/import-codesign-certs@v2
- with:
- keychain-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
- p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
-
- - name: Install the Apple distribution certificate and provisioning profile
- uses: apple-actions/import-codesign-certs@v2
- with:
- create-keychain: false # do not create a new keychain for this value
- keychain-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- p12-file-base64: ${{ secrets.DEV_CERTIFICATES_P12 }}
- p12-password: ${{ secrets.DEV_CERTIFICATES_P12_PASSWORD }}
- # - name: Bump Version Number
- # run: |
- - name: Build Binaries
- run: |
- cd iOS_SDK/OneSignalSDK
- chmod +x ./build_all_frameworks.sh
- ./build_all_frameworks.sh
- shell: bash
- - name: Code Sign
- run: |
- cd iOS_SDK/OneSignalSDK
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Core/OneSignalCore.xcframework
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Extension/OneSignalExtension.xcframework
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_InAppMessages/OneSignalInAppMessages.xcframework
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Location/OneSignalLocation.xcframework
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Notifications/OneSignalNotifications.xcframework
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_OSCore/OneSignalOSCore.xcframework
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Outcomes/OneSignalOutcomes.xcframework
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_User/OneSignalUser.xcframework
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_XCFramework/OneSignalFramework.xcframework
- codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_LiveActivities/OneSignalLiveActivities.xcframework
- shell: bash
- - name: Update Swift Package
- run: |
- cd iOS_SDK/OneSignalSDK
- chmod +x ./update_swift_package.sh
- ./update_swift_package.sh ${{github.event.inputs.version}}
- shell: bash
- - name: Commit Changes
- run: |
- git config --local user.email "noreply@onesignal.com"
- git config --local user.name "SyncR 🤖"
- git add .
- git commit -m "Release ${{github.event.inputs.version}}"
-
- - name: Pushing changes
- uses: ad-m/github-push-action@master
- with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
- repository: 'OneSignal/OneSignal-iOS-SDK'
- force: true
- branch: ${{github.event.inputs.release_branch}}
-
- - name: "Submitting PR"
- uses: octokit/request-action@v2.x
- with:
- route: POST /repos/{owner}/{repo}/pulls
- owner: OneSignal
- repo: OneSignal-iOS-SDK
- head: ${{github.event.inputs.release_branch}}
- base: main
- title: |
- "Release ${{github.event.inputs.version}}"
- body: |
- "Add Release Notes For Review Here"
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e8163d6c2..b66852d46 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Select Xcode Version
run: |
- sudo xcode-select -s /Applications/Xcode_15.2.app
+ sudo xcode-select -s /Applications/Xcode_16.4.0.app
- name: Checkout OneSignal-iOS-SDK
uses: actions/checkout@v3
- name: Set Default Scheme
diff --git a/.github/workflows/create-github-release.yml b/.github/workflows/create-github-release.yml
new file mode 100644
index 000000000..acccfec5c
--- /dev/null
+++ b/.github/workflows/create-github-release.yml
@@ -0,0 +1,89 @@
+name: Create GitHub Release
+
+# This workflow creates a GitHub release in iOS-SDK and attaches the built zip files.
+# Runs automatically when a release PR is merged.
+
+on:
+ pull_request:
+ types:
+ - closed
+ branches:
+ - main
+ - '*-main' # Matches version branches like 5.3-main
+
+permissions:
+ contents: write
+ pull-requests: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ # Step 1: Extract version from podspec
+ get-version:
+ if: |
+ github.event.pull_request.merged == true &&
+ contains(github.event.pull_request.title, 'Release')
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.extract_version.outputs.version }}
+ steps:
+ - name: Checkout OneSignal-iOS-SDK
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.base.ref }}
+
+ - name: Extract release version from podspec
+ id: extract_version
+ run: |
+ VERSION=$(grep -E "s.version\s*=" OneSignal.podspec | sed -E 's/.*"(.*)".*/\1/')
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Extracted version: $VERSION"
+
+ # Step 2: Use reusable workflow to create GitHub release with release notes
+ create-release:
+ needs: get-version
+ uses: OneSignal/sdk-shared/.github/workflows/github-release.yml@main
+ with:
+ version: ${{ needs.get-version.outputs.version }}
+
+ # Step 3: Upload the 10 xcframework zips to the release
+ upload-assets:
+ needs: [get-version, create-release]
+ runs-on: ubuntu-latest
+
+ env:
+ VERSION: ${{ needs.get-version.outputs.version }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ steps:
+ - name: Checkout OneSignal-iOS-SDK
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.base.ref }}
+
+ - name: 📋 Display Configuration
+ run: |
+ echo "============================================"
+ echo "📦 Uploading assets for version: $VERSION"
+ echo "============================================"
+
+ - name: Upload xcframework zips to release
+ run: |
+ cd iOS_SDK/OneSignalSDK
+
+ gh release upload "$VERSION" \
+ OneSignal_Core/OneSignalCore.xcframework.zip \
+ OneSignal_Extension/OneSignalExtension.xcframework.zip \
+ OneSignal_XCFramework/OneSignalFramework.xcframework.zip \
+ OneSignal_InAppMessages/OneSignalInAppMessages.xcframework.zip \
+ OneSignal_LiveActivities/OneSignalLiveActivities.xcframework.zip \
+ OneSignal_Location/OneSignalLocation.xcframework.zip \
+ OneSignal_Notifications/OneSignalNotifications.xcframework.zip \
+ OneSignal_OSCore/OneSignalOSCore.xcframework.zip \
+ OneSignal_Outcomes/OneSignalOutcomes.xcframework.zip \
+ OneSignal_User/OneSignalUser.xcframework.zip
+
+ echo "✅ All xcframework zips uploaded successfully!"
+ echo "🔗 https://github.com/${{ github.repository }}/releases/tag/$VERSION"
diff --git a/.github/workflows/create-release-prs.yml b/.github/workflows/create-release-prs.yml
new file mode 100644
index 000000000..1e740dcd8
--- /dev/null
+++ b/.github/workflows/create-release-prs.yml
@@ -0,0 +1,235 @@
+name: Create Release PRs
+
+# Note: The "Use workflow from" dropdown selects which version of THIS workflow to run.
+# Always select "main" unless you're testing workflow changes from another branch.
+# The "base_branch" input below determines where the release PR will be targeted.
+
+# This workflow bumps version and creates PRs in the iOS-SDK and XCFramework SDK.
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "The version number of the release (e.g., 5.2.15 or 5.2.3-beta-01)"
+ type: string
+ required: true
+ base_branch:
+ description: "Target branch for the PR (e.g. main for regular releases, 5.3-main for 5.3.x releases)"
+ type: string
+ required: false
+ default: "main"
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ # Step 1: Use reusable workflow to prepare release branch
+ prep:
+ uses: OneSignal/sdk-shared/.github/workflows/prep-release.yml@main
+ with:
+ version: ${{ github.event.inputs.version }}
+ target_branch: ${{ github.event.inputs.base_branch }}
+
+ # Step 2: Update iOS-specific files and build binaries
+ update-and-build:
+ needs: prep
+ runs-on: macos-14
+
+ outputs:
+ version_from: ${{ steps.extract_version.outputs.current_version }}
+
+ env:
+ VERSION: ${{ github.event.inputs.version }}
+ BASE_BRANCH: ${{ github.event.inputs.base_branch }}
+ RELEASE_BRANCH: ${{ needs.prep.outputs.release_branch }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ steps:
+ - name: 📋 Display Configuration
+ run: |
+ echo "============================================"
+ echo "📦 Release Version: $VERSION"
+ echo "🎯 Base Branch (PR Target): $BASE_BRANCH"
+ echo "🌿 Release Branch: $RELEASE_BRANCH"
+ echo "============================================"
+
+ - name: Checkout OneSignal-iOS-SDK
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ needs.prep.outputs.release_branch }}
+ fetch-depth: 0
+
+ - name: Setup Git User
+ uses: OneSignal/sdk-shared/.github/actions/setup-git-user@main
+
+ - name: Extract Current Version
+ id: extract_version
+ run: |
+ CURRENT_VERSION=$(grep -E "s.version\s*=" OneSignal.podspec | sed -E 's/.*"(.*)".*/\1/')
+ echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
+ echo "Current version: $CURRENT_VERSION"
+
+ - name: Setup Xcode
+ uses: maxim-lobanov/setup-xcode@v1
+ with:
+ xcode-version: "15.2"
+
+ - name: Install the Apple distribution certificate and provisioning profile (OneSignal)
+ uses: apple-actions/import-codesign-certs@v2
+ with:
+ keychain-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
+ p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
+ p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
+
+ - name: Install the Apple distribution certificate and provisioning profile (Lilomi)
+ uses: apple-actions/import-codesign-certs@v2
+ with:
+ create-keychain: false # do not create a new keychain for this value
+ keychain-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
+ p12-file-base64: ${{ secrets.DEV_CERTIFICATES_P12 }}
+ p12-password: ${{ secrets.DEV_CERTIFICATES_P12_PASSWORD }}
+
+ - name: Update Version in SDK and Podspec Files
+ run: |
+ cd iOS_SDK/OneSignalSDK
+ chmod +x ./update_version.sh
+ ./update_version.sh $VERSION
+ shell: bash
+
+ - name: Commit Version Bump and Push Changes
+ run: |
+ git commit -am "chore: bump version to $VERSION"
+ git push origin $RELEASE_BRANCH
+
+ - name: Build Binaries
+ run: |
+ cd iOS_SDK/OneSignalSDK
+ chmod +x ./build_all_frameworks.sh
+ ./build_all_frameworks.sh
+ shell: bash
+
+ - name: Code Sign
+ run: |
+ cd iOS_SDK/OneSignalSDK
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Core/OneSignalCore.xcframework
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Extension/OneSignalExtension.xcframework
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_InAppMessages/OneSignalInAppMessages.xcframework
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Location/OneSignalLocation.xcframework
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Notifications/OneSignalNotifications.xcframework
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_OSCore/OneSignalOSCore.xcframework
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_Outcomes/OneSignalOutcomes.xcframework
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_User/OneSignalUser.xcframework
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_XCFramework/OneSignalFramework.xcframework
+ codesign --timestamp -v --sign "Apple Distribution: OneSignal, Inc. (J3J28YJX9L)" OneSignal_LiveActivities/OneSignalLiveActivities.xcframework
+ shell: bash
+
+ - name: Commit Build and Push Changes
+ run: |
+ git commit -am "chore: build binaries"
+ git push origin $RELEASE_BRANCH
+
+ - name: Update Swift Package
+ run: |
+ cd iOS_SDK/OneSignalSDK
+ chmod +x ./update_swift_package.sh
+ ./update_swift_package.sh $VERSION
+ shell: bash
+
+ - name: Commit Swift Package and Push Changes
+ run: |
+ git add .
+ git commit -m "chore: update Swift package"
+ git push origin $RELEASE_BRANCH
+
+ # Step 3: Use reusable workflow to create iOS SDK PR with release notes
+ create-ios-pr:
+ needs: [prep, update-and-build]
+ uses: OneSignal/sdk-shared/.github/workflows/create-release.yml@main
+ with:
+ release_branch: ${{ needs.prep.outputs.release_branch }}
+ target_branch: ${{ github.event.inputs.base_branch }}
+
+ # Step 4: Update XCFramework repository
+ update-xcframework:
+ needs: [prep, update-and-build, create-ios-pr]
+ runs-on: macos-14
+
+ env:
+ VERSION: ${{ github.event.inputs.version }}
+ BASE_BRANCH: ${{ github.event.inputs.base_branch }}
+ RELEASE_BRANCH: ${{ needs.prep.outputs.release_branch }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ steps:
+ - name: Checkout OneSignal-iOS-SDK
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ needs.prep.outputs.release_branch }}
+ path: ios-sdk-repo
+
+ - name: Get iOS SDK PR Body
+ run: |
+ cd ios-sdk-repo
+
+ # Find the PR that was just created for this version
+ PR_NUMBER=$(gh pr list --state open --search "Release $VERSION in:title" --json number --jq '.[0].number // empty')
+
+ if [[ -n "$PR_NUMBER" ]]; then
+ echo "Found iOS SDK PR: #$PR_NUMBER"
+ gh pr view "$PR_NUMBER" --json body --jq '.body' > ../pr_body.md
+ else
+ echo "Warning: Could not find iOS SDK PR, using default body"
+ echo "## Release $VERSION" > ../pr_body.md
+ echo "" >> ../pr_body.md
+ echo "See iOS SDK release: https://github.com/OneSignal/OneSignal-iOS-SDK/releases/tag/$VERSION" >> ../pr_body.md
+ fi
+
+ - name: Checkout OneSignal-XCFramework
+ uses: actions/checkout@v4
+ with:
+ repository: OneSignal/OneSignal-XCFramework
+ ref: ${{ env.BASE_BRANCH }}
+ path: xcframework-repo
+ token: ${{ secrets.PAT_TOKEN_ONESIGNAL_XCFRAMEWORK }}
+
+ - name: Update Package.swift in XCFramework Repository
+ run: |
+ # Copy Package.swift from iOS SDK to XCFramework repo
+ cp ios-sdk-repo/Package.swift xcframework-repo/Package.swift
+
+ # Update package name to OneSignalXCFramework (only line 7, the package declaration)
+ sed -i '' '7s/name: "OneSignalFramework"/name: "OneSignalXCFramework"/' xcframework-repo/Package.swift
+
+ # Navigate to XCFramework repo
+ cd xcframework-repo
+
+ # Delete remote branch if it exists
+ git push origin --delete $RELEASE_BRANCH || true
+
+ # Create release branch
+ git checkout -b $RELEASE_BRANCH
+
+ # Configure git
+ git config --local user.email "noreply@onesignal.com"
+ git config --local user.name "github-actions[bot]"
+
+ # Commit changes
+ git commit -am "Release $VERSION"
+
+ # Push to remote
+ git push origin $RELEASE_BRANCH
+ env:
+ GH_TOKEN: ${{ secrets.PAT_TOKEN_ONESIGNAL_XCFRAMEWORK }}
+
+ - name: Create Pull Request for XCFramework Repository
+ env:
+ GH_TOKEN: ${{ secrets.PAT_TOKEN_ONESIGNAL_XCFRAMEWORK }}
+ run: |
+ cd xcframework-repo
+
+ gh pr create \
+ --title "Release $VERSION" \
+ --body-file ../pr_body.md \
+ --head "$RELEASE_BRANCH" \
+ --base "$BASE_BRANCH"
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 000000000..589c6e523
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,94 @@
+name: E2E Tests
+
+on:
+ push:
+ branches:
+ - rel/**
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build-ios:
+ runs-on: macos-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Set up demo
+ uses: ./.github/actions/setup-demo
+ with:
+ onesignal-app-id: ${{ vars.APPIUM_ONESIGNAL_APP_ID }}
+ onesignal-api-key: ${{ secrets.APPIUM_ONESIGNAL_API_KEY }}
+
+ - name: Cache Xcode DerivedData
+ uses: actions/cache@v5
+ with:
+ path: examples/demo/build
+ key: deriveddata-${{ runner.os }}-${{ hashFiles('examples/demo/project.yml', 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj') }}
+ restore-keys: deriveddata-${{ runner.os }}-
+
+ - name: Set up iOS codesigning
+ uses: OneSignal/sdk-shared/.github/actions/setup-ios-demo-codesigning@main
+ with:
+ p12-base64: ${{ secrets.APPIUM_IOS_DEV_CERT_P12_BASE64 }}
+ p12-password: ${{ secrets.APPIUM_IOS_DEV_CERT_PASSWORD }}
+ asc-key-id: ${{ secrets.APPIUM_APP_STORE_CONNECT_KEY_ID }}
+ asc-issuer-id: ${{ secrets.APPIUM_APP_STORE_CONNECT_ISSUER_ID }}
+ asc-private-key: ${{ secrets.APPIUM_APP_STORE_CONNECT_PRIVATE_KEY }}
+
+ - name: Build signed IPA
+ run: |
+ xcodebuild archive \
+ -workspace iOS_SDK/OneSignalSDK.xcworkspace \
+ -scheme App \
+ -configuration Release \
+ -sdk iphoneos \
+ -destination 'generic/platform=iOS' \
+ -archivePath examples/demo/build/App.xcarchive \
+ -derivedDataPath examples/demo/build \
+ -quiet \
+ -hideShellScriptEnvironment \
+ CODE_SIGN_STYLE=Manual \
+ COMPILER_INDEX_STORE_ENABLE=NO
+ xcodebuild -exportArchive \
+ -archivePath examples/demo/build/App.xcarchive \
+ -exportOptionsPlist examples/demo/ExportOptions.plist \
+ -exportPath examples/demo/build/ipa \
+ -quiet
+
+ - name: Verify aps-environment in IPA
+ working-directory: examples/demo
+ run: |
+ IPA=$(ls build/ipa/*.ipa | head -n1)
+ unzip -oq "$IPA" -d /tmp/ipa
+ APP=$(ls -d /tmp/ipa/Payload/*.app | head -n1)
+ codesign -d --entitlements - "$APP" 2>&1 | tee /tmp/entitlements.txt
+ if ! grep -q 'aps-environment' /tmp/entitlements.txt; then
+ echo "::error::Built IPA is missing aps-environment entitlement; push subscription will not work"
+ exit 1
+ fi
+
+ - name: Upload IPA
+ uses: actions/upload-artifact@v7
+ with:
+ name: demo-ipa
+ path: examples/demo/build/ipa/App.ipa
+ retention-days: 1
+ compression-level: 0
+
+ e2e-ios:
+ needs: build-ios
+ uses: OneSignal/sdk-shared/.github/workflows/appium-e2e.yml@main
+ secrets: inherit
+ with:
+ platform: ios
+ app-artifact: demo-ipa
+ app-filename: App.ipa
+ sdk-type: ios
+ build-name: ios-${{ github.ref_name }}-${{ github.run_number }}
diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml
index e4ddc5b59..da8b063f8 100644
--- a/.github/workflows/project.yml
+++ b/.github/workflows/project.yml
@@ -14,4 +14,4 @@ jobs:
with:
# SDK Mobile Project
project-url: https://github.com/orgs/OneSignal/projects/18
- github-token: ${{ secrets.GH_PROJECTS_TOKEN }}
+ github-token: ${{ secrets.GH_PUSH_TOKEN }}
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
new file mode 100644
index 000000000..0409b278a
--- /dev/null
+++ b/.github/workflows/publish-release.yml
@@ -0,0 +1,194 @@
+name: Publish Release to CocoaPods and SPM
+
+# This workflow publishes the OneSignal pods to CocoaPods trunk.
+# And creates the tagged release in the OneSignal-XCFramework repository for SPM.
+# Run this AFTER the release PR has been merged and the GitHub release has been created.
+
+on:
+ workflow_dispatch:
+ inputs:
+ ref:
+ description: "Branch or commit SHA to run on (e.g., main, 5.3-main)"
+ type: string
+ required: false
+ default: "main"
+
+permissions:
+ contents: write
+ pull-requests: write
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ publish:
+ outputs:
+ sdk_version: ${{ steps.extract_version.outputs.version }}
+ runs-on: macos-14
+
+ env:
+ COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ steps:
+ - name: Checkout OneSignal-iOS-SDK
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.inputs.ref }}
+ fetch-depth: 0
+
+ - name: Detect current branch
+ id: detect_branch
+ run: |
+ if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
+ BRANCH="${{ github.event.inputs.ref }}"
+ else
+ BRANCH="${GITHUB_REF#refs/heads/}"
+ fi
+ echo "branch=$BRANCH" >> $GITHUB_OUTPUT
+ echo "Detected branch: $BRANCH"
+
+ - name: Extract release version from podspec
+ id: extract_version
+ run: |
+ VERSION=$(grep -E "s.version\s*=" OneSignal.podspec | sed -E 's/.*"(.*)".*/\1/')
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Extracted version: $VERSION"
+
+ - name: 📋 Display Configuration
+ run: |
+ echo "============================================"
+ echo "📦 Release Version: ${{ steps.extract_version.outputs.version }}"
+ echo "🌿 Branch: ${{ steps.detect_branch.outputs.branch }}"
+ echo "============================================"
+
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: "3.0"
+
+ - name: Install CocoaPods
+ run: |
+ gem install cocoapods
+ pod --version
+
+ - name: Validate OneSignal.podspec
+ run: |
+ echo "Validating OneSignal.podspec..."
+ pod spec lint OneSignal.podspec --allow-warnings
+
+ - name: Validate OneSignalXCFramework.podspec
+ run: |
+ echo "Validating OneSignalXCFramework.podspec..."
+ pod spec lint OneSignalXCFramework.podspec --allow-warnings
+
+ - name: Publish OneSignal.podspec to CocoaPods Trunk
+ run: |
+ echo "Publishing OneSignal.podspec to CocoaPods Trunk..."
+ pod trunk push OneSignal.podspec --allow-warnings
+
+ - name: Publish OneSignalXCFramework.podspec to CocoaPods Trunk
+ run: |
+ echo "Publishing OneSignalXCFramework.podspec to CocoaPods Trunk..."
+ pod trunk push OneSignalXCFramework.podspec --allow-warnings
+
+ - name: ✅ CocoaPods Published
+ run: |
+ VERSION="${{ steps.extract_version.outputs.version }}"
+ echo "============================================"
+ echo "✅ Successfully published version $VERSION to CocoaPods!"
+ echo "============================================"
+ echo "📦 OneSignal: https://cocoapods.org/pods/OneSignal"
+ echo "📦 OneSignalXCFramework: https://cocoapods.org/pods/OneSignalXCFramework"
+
+ - name: Checkout OneSignal-XCFramework
+ uses: actions/checkout@v4
+ with:
+ repository: OneSignal/OneSignal-XCFramework
+ ref: ${{ steps.detect_branch.outputs.branch }}
+ path: xcframework-repo
+ fetch-depth: 0
+ token: ${{ secrets.PAT_TOKEN_ONESIGNAL_XCFRAMEWORK }}
+
+ - name: Get iOS SDK Release Body
+ id: get_ios_release
+ run: |
+ VERSION="${{ steps.extract_version.outputs.version }}"
+
+ # Fetch the release body from OneSignal-iOS-SDK
+ if gh release view "$VERSION" --repo OneSignal/OneSignal-iOS-SDK --json body --jq '.body' > ios_release_body.md 2>/dev/null; then
+ echo "✅ Found iOS SDK release for version $VERSION"
+ echo "found=true" >> $GITHUB_OUTPUT
+ else
+ echo "⚠️ No iOS SDK release found for version $VERSION"
+ echo "found=false" >> $GITHUB_OUTPUT
+ echo "" > ios_release_body.md
+ fi
+
+ - name: Create GitHub Release for OneSignal-XCFramework
+ env:
+ GH_TOKEN: ${{ secrets.PAT_TOKEN_ONESIGNAL_XCFRAMEWORK }}
+ run: |
+ VERSION="${{ steps.extract_version.outputs.version }}"
+ BRANCH="${{ steps.detect_branch.outputs.branch }}"
+
+ cd xcframework-repo
+
+ # Configure git
+ git config --local user.email "noreply@onesignal.com"
+ git config --local user.name "github-actions[bot]"
+
+ # Create and push tag
+ git tag -a "$VERSION" -m "Release $VERSION"
+ git push origin "$VERSION"
+
+ echo "✅ Created and pushed tag: $VERSION to OneSignal-XCFramework"
+
+ # Use iOS SDK release body if available, otherwise create default release notes
+ if [[ "${{ steps.get_ios_release.outputs.found }}" == "true" ]] && [[ -s ../ios_release_body.md ]]; then
+ echo "Using release notes from iOS SDK release"
+ cp ../ios_release_body.md release_notes.md
+ else
+ echo "No iOS SDK release body found, generating default release notes"
+ echo "## 🔖 Release $VERSION" > release_notes.md
+ echo "" >> release_notes.md
+ echo "This release corresponds to [OneSignal-iOS-SDK $VERSION](https://github.com/OneSignal/OneSignal-iOS-SDK/releases/tag/$VERSION)" >> release_notes.md
+ fi
+
+ # Determine if this is a pre-release
+ PRERELEASE_FLAG=""
+ if [[ "$VERSION" == *"alpha"* ]] || [[ "$VERSION" == *"beta"* ]]; then
+ PRERELEASE_FLAG="--prerelease"
+ echo "Marking as pre-release (alpha/beta detected)"
+ fi
+
+ # Create GitHub release
+ gh release create "$VERSION" \
+ --repo OneSignal/OneSignal-XCFramework \
+ --title "$VERSION" \
+ --notes-file release_notes.md \
+ --target "$BRANCH" \
+ $PRERELEASE_FLAG
+
+ echo "✅ GitHub release created successfully for OneSignal-XCFramework!"
+ echo "🔗 https://github.com/OneSignal/OneSignal-XCFramework/releases/tag/$VERSION"
+
+ - name: ✅ All Steps Complete
+ run: |
+ VERSION="${{ steps.extract_version.outputs.version }}"
+ echo "============================================"
+ echo "✅ Successfully completed all release steps for version $VERSION"
+ echo "============================================"
+ echo "📦 CocoaPods OneSignal: https://cocoapods.org/pods/OneSignal"
+ echo "📦 CocoaPods OneSignalXCFramework: https://cocoapods.org/pods/OneSignalXCFramework"
+ echo "🔗 iOS SDK Release: https://github.com/OneSignal/OneSignal-iOS-SDK/releases/tag/$VERSION"
+ echo "🔗 XCFramework Release: https://github.com/OneSignal/OneSignal-XCFramework/releases/tag/$VERSION"
+
+ wrapper_prs:
+ needs: publish
+ uses: OneSignal/sdk-shared/.github/workflows/create-wrapper-prs.yml@main
+ secrets:
+ GH_PUSH_TOKEN: ${{ secrets.GH_PUSH_TOKEN }}
+ with:
+ ios_version: ${{ needs.publish.outputs.sdk_version }}
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
deleted file mode 100644
index 439cae130..000000000
--- a/.github/workflows/release-drafter.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: Release Drafter
-
-on:
- push:
- # branches to consider in the event; optional, defaults to all
- branches:
- - main
- # pull_request event is required only for autolabeler
- pull_request:
- # Only following types are handled by the action, but one can default to all as well
- types: [opened, reopened, synchronize]
- # pull_request_target event is required for autolabeler to support PRs from forks
- # pull_request_target:
- # types: [opened, reopened, synchronize]
-
-permissions:
- contents: read
-
-jobs:
- update_release_draft:
- permissions:
- # write permission is required to create a github release
- contents: write
- # write permission is required for autolabeler
- # otherwise, read permission is required at least
- pull-requests: write
- runs-on: ubuntu-latest
- steps:
- # (Optional) GitHub Enterprise requires GHE_HOST variable set
- #- name: Set GHE_HOST
- # run: |
- # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV
-
- # Drafts your next Release notes as Pull Requests are merged into "master"
- - uses: release-drafter/release-drafter@v5
- # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
- # with:
- # config-name: my-config.yml
- # disable-autolabeler: true
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/set_response_time.yml b/.github/workflows/set_response_time.yml
deleted file mode 100644
index 4b711ee71..000000000
--- a/.github/workflows/set_response_time.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-name: Set Response Time
-on:
- issue_comment:
- types:
- - created
- issues:
- types:
- - closed
-jobs:
- calculate:
- name: set reponse time for the issue
- if: github.event.issue.pull_request == null
- runs-on: ubuntu-latest
- permissions:
- issues: write
- steps:
- - uses: actions/checkout@v3
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
- - run: npm install @octokit/action@6.0.6
- - uses: actions/github-script@v6
- id: set-time
- with:
- result-encoding: string
- script: |
- const os_probot_metadata = require('./.github/os_probot_metadata.js')
- const set_response_time = require('./.github/set_response_times.js')
- return await set_response_time(context, os_probot_metadata)
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Get result
- run: echo "${{steps.set-time.outputs.result}}" >> $GITHUB_STEP_SUMMARY
diff --git a/.gitignore b/.gitignore
index 6174637eb..0e7dffa5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,8 @@ profile
DerivedData
.idea/
iOS_SDK/Carthage/Build
-/temp/
\ No newline at end of file
+/temp/
+.build/
+
+examples/demo/App/Secrets.plist
+examples/demo/Local.xcconfig
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 202f9299c..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-language: objective-c
-osx_image: xcode11.1
-before_install:
- - cd iOS_SDK/OneSignalSDK
-script:
- - xcodebuild -scheme UnitTests -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8 Plus,OS=13.1' test
diff --git a/GettingStarted.md b/GettingStarted.md
new file mode 100644
index 000000000..2fd265c7e
--- /dev/null
+++ b/GettingStarted.md
@@ -0,0 +1,57 @@
+# Getting Started
+
+This repo ships two ways to exercise the OneSignal iOS SDK:
+
+| App | Location | Purpose |
+|-----|----------|---------|
+| **OneSignalDevApp** | `iOS_SDK/OneSignalDevApp/` | Internal dev/test app wired into `OneSignalSDK.xcworkspace`. Builds against **local SDK source**, so any changes you make to the SDK are picked up immediately. Use this when modifying the SDK. |
+| **examples/demo** | `examples/demo/` | Customer-facing SwiftUI demo that mirrors the OneSignal Capacitor / Cordova / RN demos (same section layout, accessibility identifiers, sdk-shared tooltip content). Builds against the published SwiftPM SDK. Use this as a reference integration. |
+
+## Prerequisites
+
+| Requirement | Minimum Version |
+|-------------|-----------------|
+| macOS | 13 Ventura+ |
+| Xcode | 15.0+ |
+| Swift | 5.9+ |
+| iOS target | 16.0+ |
+
+## Running OneSignalDevApp (SDK contributors)
+
+This is the recommended path when you're working on the SDK itself.
+
+1. Open `iOS_SDK/OneSignalSDK.xcworkspace` in Xcode (the workspace, not any individual `.xcodeproj`).
+2. Select the **OneSignalDevApp** scheme.
+3. Pick a simulator or a connected device.
+4. Press **Cmd + R** to build and run.
+
+SDK debug logs stream to the Xcode console — useful for verifying network calls, subscription state, and in-app message events.
+
+> Push notification delivery requires a **physical device** with a valid APNs configuration. The simulator supports permission prompts and token generation but won't receive remote pushes.
+
+## Running examples/demo (reference integration)
+
+The `examples/demo/` app demonstrates the recommended integration shape for app developers, including a Notification Service Extension target and a Live Activities Widget Extension target.
+
+See [`examples/demo/README.md`](examples/demo/README.md) for full setup steps. In short:
+
+1. Create the Xcode project at `examples/demo/App.xcodeproj` (the source files and extension folders are checked in but `project.pbxproj` is not).
+2. Add the OneSignal SwiftPM dependency (`https://github.com/OneSignal/OneSignal-iOS-SDK`, 5.0.0+) and attach the right products to each of the three targets (App / NSE / Widget).
+3. Configure capabilities (Push Notifications, App Groups, Background Modes → Remote notifications) and run.
+
+## Using your own App ID
+
+Both apps default to a shared OneSignal App ID. To switch to your own:
+
+- **OneSignalDevApp** — open `iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m` and replace the App ID passed to `OneSignal.initialize`.
+- **examples/demo** — edit `examples/demo/App/Services/OneSignalService.swift` and replace `defaultAppId`, or override at runtime via `UserDefaults` (key `OneSignalAppId`).
+
+Changing the App ID requires uninstalling and reinstalling the app for it to take effect.
+
+## Troubleshooting
+
+| Problem | Fix |
+|---------|-----|
+| Build fails with missing framework | Open the workspace (`OneSignalSDK.xcworkspace`), not an individual `.xcodeproj`. |
+| Push notifications don't arrive on simulator | Push delivery requires a physical device with APNs configured. |
+| "Consent Required" blocks SDK calls | Toggle **Consent Required** off, or grant consent via the SDK's consent API. |
diff --git a/README.md b/README.md
index a5657ea14..8c2c639bc 100644
--- a/README.md
+++ b/README.md
@@ -7,28 +7,21 @@
---
-#### ⚠️ Migration Advisory for current OneSignal customers
+#### Migrating from v4 or earlier?
-Our new [user-centric APIs and v5.x.x SDKs](https://onesignal.com/blog/unify-your-users-across-channels-and-devices/) offer an improved user and data management experience. However, they may not be at 1:1 feature parity with our previous versions yet.
-
-If you are migrating an existing app, we suggest using iOS Phased Rollout capabilities to ensure that there are no unexpected issues or edge cases. Here is the documentation:
-
-- [iOS Phased Rollout](https://developer.apple.com/help/app-store-connect/update-your-app/release-a-version-update-in-phases/)
-
-
-If you run into any challenges or have concerns, please contact our support team at support@onesignal.com
+See our [Migration Guide](MIGRATION_GUIDE.md) for detailed instructions on upgrading to v5.x.x.
---
[OneSignal](https://www.onesignal.com) is a free email, sms, push notification, and in-app message service for mobile apps. This plugin makes it easy to integrate your native iOS app with OneSignal.
-

+
#### Installation
See OneSignal's [iOS Native SDK Setup Guide](https://documentation.onesignal.com/docs/ios-sdk-setup) for documentation.
#### API
-See OneSignal's [iOS Native SDK API](https://documentation.onesignal.com/docs/ios-native-sdk) page for a list of all available methods.
+See OneSignal's [Mobile SDK reference](https://documentation.onesignal.com/docs/en/mobile-sdk-reference) page for a list of all available methods.
#### Change Log
See this repository's [release tags](https://github.com/OneSignal/OneSignal-iOS-SDK/releases) for a complete change log of every released version.
@@ -39,4 +32,4 @@ For account issues and support please contact OneSignal support from the [OneSig
#### Supports:
* Swift and Objective-C Projects
-* Supports iOS 9 to iOS 15
+* Supports iOS 12 to iOS 26
diff --git a/default b/default
deleted file mode 100644
index 47a2b865b..000000000
--- a/default
+++ /dev/null
@@ -1 +0,0 @@
-UnitTestApp
diff --git a/examples/demo/App.entitlements b/examples/demo/App.entitlements
new file mode 100644
index 000000000..344636495
--- /dev/null
+++ b/examples/demo/App.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ aps-environment
+ development
+ com.apple.security.application-groups
+
+ group.com.onesignal.example.onesignal
+
+
+
diff --git a/examples/demo/App.xcodeproj/project.pbxproj b/examples/demo/App.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..c2eb9fe65
--- /dev/null
+++ b/examples/demo/App.xcodeproj/project.pbxproj
@@ -0,0 +1,1230 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 00B9E4C45782A6AD1CD3FE48 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2F82BD207897EB78739182A /* OneSignalExtension.framework */; };
+ 08C5E83ABC14EC2FC88276B9 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */; };
+ 0BC1978B56970258E20C24C4 /* SendPushSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B81D7E1A7EB9BB4466C768F /* SendPushSection.swift */; };
+ 0C4F40193331204CC2E05743 /* OneSignalNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */; };
+ 0E6E9EEBF2500A4E70022960 /* UserFetchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FD8258E807E6672642A32E6 /* UserFetchService.swift */; };
+ 0E7D0439A19C8BF291A8BEB1 /* EmailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1892E9B40F0E8FB23DD64206 /* EmailsSection.swift */; };
+ 12597AC14E1783CC87D6E147 /* OneSignalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */; };
+ 14228D0C997F9168643F8154 /* OutcomesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4078B5F096680AFA83D1CB85 /* OutcomesSection.swift */; };
+ 1653658B1F4EE21203BBAA5D /* TriggersSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1155DE423DAA8B5396948C9B /* TriggersSection.swift */; };
+ 16B0C854BC2498BEEDC4EEED /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E7F504B0421F2B6247E2F5 /* Theme.swift */; };
+ 1A247F505A707756873AA9FA /* AddItemDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D84B9C2B0EFE086A8E48CD /* AddItemDialog.swift */; };
+ 1F417CAD2528616703CAA54D /* ToastPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6CBCD47A3EA4209A6DDB03 /* ToastPresenter.swift */; };
+ 1FF9F70882A0CF6A73416DDF /* OneSignalWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0864BD4A6F62539B2809338F /* OneSignalWidgetLiveActivity.swift */; };
+ 2015D767360C96D01A2FF3D9 /* OneSignalCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 20792A9930A201E187AA0ABF /* PreferencesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76153AD2F6C6EB8F77138B9 /* PreferencesService.swift */; };
+ 232A52A5D5719F863641166C /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2F82BD207897EB78739182A /* OneSignalExtension.framework */; };
+ 25C963476DF0B01BAABE24D9 /* TooltipService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939E1F476EE48B9833A3311C /* TooltipService.swift */; };
+ 27C72DF35BE082E3E1093F75 /* SendIamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60D09DC8C809877FB4BF465 /* SendIamSection.swift */; };
+ 2859C7827BE28C7EE4AFDAB4 /* CustomNotificationDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D959B1636916DAEE5FE6278 /* CustomNotificationDialog.swift */; };
+ 28D491D31B5C07E4D4F48A7D /* SecretsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EBF32FC1DBF3C34188E08C /* SecretsConfig.swift */; };
+ 2FA8128D2A921DDF02210D8A /* RemoveMultiDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A654457BF0A55B54220E669 /* RemoveMultiDialog.swift */; };
+ 2FCC417641D480849E99588B /* SmsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E194A3F19072CB17A8F1A12E /* SmsSection.swift */; };
+ 3038C8C43A465DFED77AA533 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B24E059F4DF6ABED55BA3183 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 3927A4BF207695E98A57E445 /* TagsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */; };
+ 39D2C94F79A62BFF9DE5DBA9 /* OutcomeDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */; };
+ 3C899E2494DE29756F5451BE /* OneSignalNotifications.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 3FB3C8C0634A765EE81D042E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 76989E05CECAD7B8B3C424A7 /* Assets.xcassets */; };
+ 4414A3304BAEB384B8D16D09 /* OneSignalExtension.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B2F82BD207897EB78739182A /* OneSignalExtension.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 4C18E3D284BB28BD846162F3 /* NotificationSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38138523A8A81A60A77800CA /* NotificationSender.swift */; };
+ 56DF63011CB625810F81075D /* OneSignalWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD14C3CEEDFB9557E589B45 /* OneSignalWidgetBundle.swift */; };
+ 5737CABFA55E019B2732B90D /* OneSignalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F1FA3F3D16A857DB6ED045F /* OneSignalViewModel.swift */; };
+ 5B959D44AB09CB821C00AFBF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F726E64F9B6817F917227C /* ContentView.swift */; };
+ 5F4B7EC8437D1A8D80DF7674 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20B46F63592FC67B655BEB8 /* NotificationService.swift */; };
+ 638B81D9DA5FD8636BB038B0 /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF49509A218369322ECFA3B9 /* OneSignalUser.framework */; };
+ 674995A7A55C13341317E19B /* OSDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D261D46C404E325CBA87A9E0 /* OSDialog.swift */; };
+ 68BC99D15FDCB26EB35EBB07 /* OneSignalLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE9834773C437CC373607693 /* OneSignalLocation.framework */; };
+ 6E3E040FD8A750248E70E46F /* AppModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EA9D80191548D49F09D30B3 /* AppModels.swift */; };
+ 7B02F364CA25825E50B09CDB /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */; };
+ 7B94F48C31E0BEA4B8CB20E2 /* SectionCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497484E7C094D645338BD404 /* SectionCard.swift */; };
+ 7C904CE2F4C2A2083324BBEB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DFACB9F304B9374F3C570 /* ToastView.swift */; };
+ 7D2BA9022E77B00205453467 /* LiveActivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0542854462194E28D9E4638D /* LiveActivityController.swift */; };
+ 7EBB68E75FAE49C09C048251 /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EF49509A218369322ECFA3B9 /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 8068AFC608E7E82F06733BE7 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */; };
+ 80E3E2B5438CFEBE1316FA84 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */; };
+ 837FCE7A095ED1D7CCFEACF3 /* InAppSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76FFAF18177241F4A47FE23 /* InAppSection.swift */; };
+ 8E57965CF0E9F1C341E61996 /* OneSignal-Dynamic.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 25848A5B7E93DCA744373200 /* OneSignal-Dynamic.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 8EA2FF24D93691FDC1661913 /* CustomEventsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6FF0BC430F5A2B89215967 /* CustomEventsSection.swift */; };
+ 902A116B26B8ECAD8EE29C95 /* ToggleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911376C90AA43F41478596FE /* ToggleRow.swift */; };
+ 93104E0838915DD6194805D5 /* Secrets.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6B00BC406653BC6B08ECCE26 /* Secrets.plist */; };
+ 99C141A5ED972D66B5CD255A /* OneSignalWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AB92C9C455B15E0B571A1C66 /* OneSignalWidget.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 9A7BE456B679D7DE7CA300BC /* ListWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 280B23B41935EAB89C8C6FCB /* ListWidgets.swift */; };
+ 9B0D1DD32B99602629BBCB95 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6094499773760C3190F62C9 /* ActionButton.swift */; };
+ 9FCE157075859B954814F6B7 /* OneSignalLiveActivities.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A2B1975BB925DCA45327D71E /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB04A33912347325A0155D6 /* App.swift */; };
+ A2E180ADEA246FB6530E5C8C /* TooltipDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38067B9DDA25E12809D9A245 /* TooltipDialog.swift */; };
+ A7A5153D68A4967A88B2B433 /* AliasesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225DEBDFE699D266D5BDE7ED /* AliasesSection.swift */; };
+ A8498B9A2AA7DA8CB0856CAC /* TrackEventDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432444EA41C495988DFAB422 /* TrackEventDialog.swift */; };
+ AB963AA6733C32EFB84DCBC0 /* vine_boom.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1EE449E8308FCB038408D7CF /* vine_boom.wav */; };
+ AD514855D19B2C249269C7D2 /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */; };
+ AF8A33E8CFAF1A8DB1907130 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */; };
+ B3AE701075398C6A369DBBE0 /* LocationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0EE23773C63B6EC3FB563A /* LocationSection.swift */; };
+ C31F603E294D69E30CE65EA3 /* OneSignalInAppMessages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */; };
+ C6E9E2C6716059856A1AC571 /* OneSignalInAppMessages.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ CAB90AEC123A57B401881DBC /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ CE2C54D552E42C2538FB95C4 /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2383E8AA63BF04ADA2CF0D /* UserSection.swift */; };
+ D154E0266C8AB372F928D42A /* OneSignalLocation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE9834773C437CC373607693 /* OneSignalLocation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ D65AC6EC014557264E7A3697 /* OneSignal-Dynamic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25848A5B7E93DCA744373200 /* OneSignal-Dynamic.framework */; };
+ DBDA727D6317A3CCC73A1699 /* LiveActivitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27856D76807C31B23CD10CFD /* LiveActivitySection.swift */; };
+ E3725231A3FD5F5A88BAA758 /* MultiPairInputDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */; };
+ E5671E3A84219D49E45F8DCF /* KeyValueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5984B93007C6B85AFE09045A /* KeyValueRow.swift */; };
+ E72F68A7DB51347AB8B10FA7 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */; };
+ EA93E372AA3E66073487B89C /* PushSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42B5EDA3264E3D00A5CB265 /* PushSection.swift */; };
+ F1DBBD7F18CB70C5DF2FFC32 /* AppSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB548D4688766660864F581 /* AppSection.swift */; };
+ FB0472C73D007EABFD01CCA8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C360043C7B20683C2E3FA3B6 /* Assets.xcassets */; };
+ FFD2A54AE34AEADBBAE5B7E3 /* OneSignalOutcomes.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 005EC764DD2429AF6136BB9A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DEBAAE272A4211D900BF2C1C;
+ remoteInfo = OneSignalInAppMessages;
+ };
+ 06313C80EB92E867CF4AE55C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DE7D17F927026BA3002D3A5D;
+ remoteInfo = OneSignalExtension;
+ };
+ 13BDB99578D2E19A9A750DEF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE69E19A282ED8060090BB3D;
+ remoteInfo = OneSignalUser;
+ };
+ 21265D9F7D2B1FBDDD3E716F /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D17F827026BA3002D3A5D;
+ remoteInfo = OneSignalExtension;
+ };
+ 25CC6D5CA1E9DA9142715E8A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = 3E2400381D4FFC31008BDE70;
+ remoteInfo = OneSignalFramework;
+ };
+ 5C38CF82FCDF8C89EA732187 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = 3E2400371D4FFC31008BDE70;
+ remoteInfo = OneSignalFramework;
+ };
+ 5C4A42186FB7137C13DD3C0D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DEBAADF82A420A3700BF2C1C;
+ remoteInfo = OneSignalLocation;
+ };
+ 642A3E9AC0D14239CE4A128E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6B590BF25178DC7D824D09CE /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97373B3889FBDD1762E98B03;
+ remoteInfo = OneSignalWidget;
+ };
+ 6701C77339EC73AE4FEC7E32 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D187F27037F43002D3A5D;
+ remoteInfo = OneSignalOutcomes;
+ };
+ 67BDC3D5140D6CC33418AA1D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D187F27037F43002D3A5D;
+ remoteInfo = OneSignalOutcomes;
+ };
+ 6DE922175F2D1304874645CD /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = 3C115160289A259500565C41;
+ remoteInfo = OneSignalOSCore;
+ };
+ 8475704534409C59DAE8B29A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D17F827026BA3002D3A5D;
+ remoteInfo = OneSignalExtension;
+ };
+ 852E98FC9C8818AE6B636551 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DEF784292912DEB600A1F3A5;
+ remoteInfo = OneSignalNotifications;
+ };
+ 8B6EE756CF7175F38D5B5B4B /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = 3C115161289A259500565C41;
+ remoteInfo = OneSignalOSCore;
+ };
+ 8E39421BCBE5C21C1A9CB242 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = 475F471E2B8E398D00EC05B3;
+ remoteInfo = OneSignalLiveActivities;
+ };
+ 996095BEB3FD8CE51B7F1AD7 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DEBAADF92A420A3700BF2C1C;
+ remoteInfo = OneSignalLocation;
+ };
+ 9CA6E55F92E787A8186EEBA1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D17E527026B95002D3A5D;
+ remoteInfo = OneSignalCore;
+ };
+ A668D6C94AC1F182AF01EAFF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6B590BF25178DC7D824D09CE /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 61033D7807F09753830EDBC1;
+ remoteInfo = OneSignalNotificationServiceExtension;
+ };
+ B627D4859D5F7B2B7F452917 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D17E527026B95002D3A5D;
+ remoteInfo = OneSignalCore;
+ };
+ C43833A058207841248D7506 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DEBAAE282A4211D900BF2C1C;
+ remoteInfo = OneSignalInAppMessages;
+ };
+ D1E066F0069BD9B244E196E4 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DEF784282912DEB600A1F3A5;
+ remoteInfo = OneSignalNotifications;
+ };
+ D378765DB7FB67E222BB7FF1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = 475F471D2B8E398D00EC05B3;
+ remoteInfo = OneSignalLiveActivities;
+ };
+ D399497AA4F221AD92C859AE /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DE7D17E627026B95002D3A5D;
+ remoteInfo = OneSignalCore;
+ };
+ D47A29D1FE2C196E0715923B /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DE69E19B282ED8060090BB3D;
+ remoteInfo = OneSignalUser;
+ };
+ E36EAB44E3DD77FE80F2A724 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DE7D188027037F43002D3A5D;
+ remoteInfo = OneSignalOutcomes;
+ };
+ E48045948F131DB2BD8C1B2A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = 475F471D2B8E398D00EC05B3;
+ remoteInfo = OneSignalLiveActivities;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 7C6B2DB89D01A1E9B8F2A565 /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ 3038C8C43A465DFED77AA533 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */,
+ 99C141A5ED972D66B5CD255A /* OneSignalWidget.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8EFDE6013C818852098745E7 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 2015D767360C96D01A2FF3D9 /* OneSignalCore.framework in Embed Frameworks */,
+ CAB90AEC123A57B401881DBC /* OneSignalOSCore.framework in Embed Frameworks */,
+ FFD2A54AE34AEADBBAE5B7E3 /* OneSignalOutcomes.framework in Embed Frameworks */,
+ 3C899E2494DE29756F5451BE /* OneSignalNotifications.framework in Embed Frameworks */,
+ 7EBB68E75FAE49C09C048251 /* OneSignalUser.framework in Embed Frameworks */,
+ 4414A3304BAEB384B8D16D09 /* OneSignalExtension.framework in Embed Frameworks */,
+ D154E0266C8AB372F928D42A /* OneSignalLocation.framework in Embed Frameworks */,
+ C6E9E2C6716059856A1AC571 /* OneSignalInAppMessages.framework in Embed Frameworks */,
+ 9FCE157075859B954814F6B7 /* OneSignalLiveActivities.framework in Embed Frameworks */,
+ 8E57965CF0E9F1C341E61996 /* OneSignal-Dynamic.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 0542854462194E28D9E4638D /* LiveActivityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityController.swift; sourceTree = ""; };
+ 0864BD4A6F62539B2809338F /* OneSignalWidgetLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalWidgetLiveActivity.swift; sourceTree = ""; };
+ 0F1FA3F3D16A857DB6ED045F /* OneSignalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalViewModel.swift; sourceTree = ""; };
+ 1155DE423DAA8B5396948C9B /* TriggersSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggersSection.swift; sourceTree = ""; };
+ 1892E9B40F0E8FB23DD64206 /* EmailsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailsSection.swift; sourceTree = ""; };
+ 1EE449E8308FCB038408D7CF /* vine_boom.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = vine_boom.wav; sourceTree = ""; };
+ 225DEBDFE699D266D5BDE7ED /* AliasesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliasesSection.swift; sourceTree = ""; };
+ 27856D76807C31B23CD10CFD /* LiveActivitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySection.swift; sourceTree = ""; };
+ 280B23B41935EAB89C8C6FCB /* ListWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgets.swift; sourceTree = ""; };
+ 291D83300C20BA3831824AFD /* OneSignalSDK */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OneSignalSDK; path = ../../iOS_SDK/OneSignalSDK/OneSignal.xcodeproj; sourceTree = ""; };
+ 2D959B1636916DAEE5FE6278 /* CustomNotificationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNotificationDialog.swift; sourceTree = ""; };
+ 35F726E64F9B6817F917227C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 38067B9DDA25E12809D9A245 /* TooltipDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipDialog.swift; sourceTree = ""; };
+ 38138523A8A81A60A77800CA /* NotificationSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSender.swift; sourceTree = ""; };
+ 3A654457BF0A55B54220E669 /* RemoveMultiDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMultiDialog.swift; sourceTree = ""; };
+ 3B81D7E1A7EB9BB4466C768F /* SendPushSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPushSection.swift; sourceTree = ""; };
+ 3FD8258E807E6672642A32E6 /* UserFetchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFetchService.swift; sourceTree = ""; };
+ 4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiPairInputDialog.swift; sourceTree = ""; };
+ 4078B5F096680AFA83D1CB85 /* OutcomesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutcomesSection.swift; sourceTree = ""; };
+ 432444EA41C495988DFAB422 /* TrackEventDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackEventDialog.swift; sourceTree = ""; };
+ 497484E7C094D645338BD404 /* SectionCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionCard.swift; sourceTree = ""; };
+ 49D88A349FD70A9153DD2C03 /* App.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 4B6FF0BC430F5A2B89215967 /* CustomEventsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEventsSection.swift; sourceTree = ""; };
+ 5984B93007C6B85AFE09045A /* KeyValueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueRow.swift; sourceTree = ""; };
+ 5C0EE23773C63B6EC3FB563A /* LocationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSection.swift; sourceTree = ""; };
+ 5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalService.swift; sourceTree = ""; };
+ 6B00BC406653BC6B08ECCE26 /* Secrets.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secrets.plist; sourceTree = ""; };
+ 6DD14C3CEEDFB9557E589B45 /* OneSignalWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalWidgetBundle.swift; sourceTree = ""; };
+ 76989E05CECAD7B8B3C424A7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 7EA9D80191548D49F09D30B3 /* AppModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModels.swift; sourceTree = ""; };
+ 7EB548D4688766660864F581 /* AppSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSection.swift; sourceTree = ""; };
+ 8F6CBCD47A3EA4209A6DDB03 /* ToastPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastPresenter.swift; sourceTree = ""; };
+ 911376C90AA43F41478596FE /* ToggleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleRow.swift; sourceTree = ""; };
+ 939E1F476EE48B9833A3311C /* TooltipService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipService.swift; sourceTree = ""; };
+ 9E2383E8AA63BF04ADA2CF0D /* UserSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = ""; };
+ A20B46F63592FC67B655BEB8 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
+ A42B5EDA3264E3D00A5CB265 /* PushSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSection.swift; sourceTree = ""; };
+ AB92C9C455B15E0B571A1C66 /* OneSignalWidget.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = OneSignalWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ B24E059F4DF6ABED55BA3183 /* OneSignalNotificationServiceExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = OneSignalNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ B3E7F504B0421F2B6247E2F5 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; };
+ B76FFAF18177241F4A47FE23 /* InAppSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppSection.swift; sourceTree = ""; };
+ BDB04A33912347325A0155D6 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
+ C360043C7B20683C2E3FA3B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ C60D09DC8C809877FB4BF465 /* SendIamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendIamSection.swift; sourceTree = ""; };
+ C76153AD2F6C6EB8F77138B9 /* PreferencesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesService.swift; sourceTree = ""; };
+ D261D46C404E325CBA87A9E0 /* OSDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDialog.swift; sourceTree = ""; };
+ D4EBF32FC1DBF3C34188E08C /* SecretsConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretsConfig.swift; sourceTree = ""; };
+ D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutcomeDialog.swift; sourceTree = ""; };
+ D6094499773760C3190F62C9 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; };
+ D8D84B9C2B0EFE086A8E48CD /* AddItemDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemDialog.swift; sourceTree = ""; };
+ E194A3F19072CB17A8F1A12E /* SmsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmsSection.swift; sourceTree = ""; };
+ ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsSection.swift; sourceTree = ""; };
+ F46DFACB9F304B9374F3C570 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; };
+ "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 83824651FEF6A3650CC5A580 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 80E3E2B5438CFEBE1316FA84 /* OneSignalCore.framework in Frameworks */,
+ AD514855D19B2C249269C7D2 /* OneSignalOutcomes.framework in Frameworks */,
+ 00B9E4C45782A6AD1CD3FE48 /* OneSignalExtension.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ BC0DC3536DED64E0E8C9923E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8068AFC608E7E82F06733BE7 /* OneSignalCore.framework in Frameworks */,
+ AF8A33E8CFAF1A8DB1907130 /* OneSignalOSCore.framework in Frameworks */,
+ 7B02F364CA25825E50B09CDB /* OneSignalOutcomes.framework in Frameworks */,
+ 0C4F40193331204CC2E05743 /* OneSignalNotifications.framework in Frameworks */,
+ 638B81D9DA5FD8636BB038B0 /* OneSignalUser.framework in Frameworks */,
+ 232A52A5D5719F863641166C /* OneSignalExtension.framework in Frameworks */,
+ 68BC99D15FDCB26EB35EBB07 /* OneSignalLocation.framework in Frameworks */,
+ C31F603E294D69E30CE65EA3 /* OneSignalInAppMessages.framework in Frameworks */,
+ E72F68A7DB51347AB8B10FA7 /* OneSignalLiveActivities.framework in Frameworks */,
+ D65AC6EC014557264E7A3697 /* OneSignal-Dynamic.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ C330DBFD3AE797742200EF8B /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 08C5E83ABC14EC2FC88276B9 /* OneSignalLiveActivities.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 19317E8C50FA7D56330C6BDF /* ViewModels */ = {
+ isa = PBXGroup;
+ children = (
+ 0F1FA3F3D16A857DB6ED045F /* OneSignalViewModel.swift */,
+ 8F6CBCD47A3EA4209A6DDB03 /* ToastPresenter.swift */,
+ );
+ path = ViewModels;
+ sourceTree = "";
+ };
+ 4102E3E068508DD683953C7D = {
+ isa = PBXGroup;
+ children = (
+ 443E9F32F0EC65EEBF466F3B /* App */,
+ E62E617DD5FF6C0AA303F74D /* OneSignalNotificationServiceExtension */,
+ C83678AA9260FD8491BCC58F /* OneSignalWidget */,
+ E9C3DB3EC6D7655028A9C0D3 /* Products */,
+ E81A0E25363045BA30E3D600 /* Projects */,
+ );
+ sourceTree = "";
+ };
+ 443E9F32F0EC65EEBF466F3B /* App */ = {
+ isa = PBXGroup;
+ children = (
+ 4B8DD72AD7356663EB6BAE3A /* Models */,
+ 72F51C4EC858B862B629616C /* Services */,
+ 19317E8C50FA7D56330C6BDF /* ViewModels */,
+ F0D08B397106EA8498C4A8F4 /* Views */,
+ BDB04A33912347325A0155D6 /* App.swift */,
+ 76989E05CECAD7B8B3C424A7 /* Assets.xcassets */,
+ 6B00BC406653BC6B08ECCE26 /* Secrets.plist */,
+ 1EE449E8308FCB038408D7CF /* vine_boom.wav */,
+ );
+ path = App;
+ sourceTree = "";
+ };
+ 4B8DD72AD7356663EB6BAE3A /* Models */ = {
+ isa = PBXGroup;
+ children = (
+ 7EA9D80191548D49F09D30B3 /* AppModels.swift */,
+ );
+ path = Models;
+ sourceTree = "";
+ };
+ 53EC9A9E14C3568A020C0977 /* Sections */ = {
+ isa = PBXGroup;
+ children = (
+ 225DEBDFE699D266D5BDE7ED /* AliasesSection.swift */,
+ 7EB548D4688766660864F581 /* AppSection.swift */,
+ 4B6FF0BC430F5A2B89215967 /* CustomEventsSection.swift */,
+ 1892E9B40F0E8FB23DD64206 /* EmailsSection.swift */,
+ B76FFAF18177241F4A47FE23 /* InAppSection.swift */,
+ 27856D76807C31B23CD10CFD /* LiveActivitySection.swift */,
+ 5C0EE23773C63B6EC3FB563A /* LocationSection.swift */,
+ 4078B5F096680AFA83D1CB85 /* OutcomesSection.swift */,
+ A42B5EDA3264E3D00A5CB265 /* PushSection.swift */,
+ C60D09DC8C809877FB4BF465 /* SendIamSection.swift */,
+ 3B81D7E1A7EB9BB4466C768F /* SendPushSection.swift */,
+ E194A3F19072CB17A8F1A12E /* SmsSection.swift */,
+ ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */,
+ 1155DE423DAA8B5396948C9B /* TriggersSection.swift */,
+ 9E2383E8AA63BF04ADA2CF0D /* UserSection.swift */,
+ );
+ path = Sections;
+ sourceTree = "";
+ };
+ 54D70151146ED2430545164A /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */,
+ 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */,
+ EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */,
+ 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */,
+ EF49509A218369322ECFA3B9 /* OneSignalUser.framework */,
+ B2F82BD207897EB78739182A /* OneSignalExtension.framework */,
+ FE9834773C437CC373607693 /* OneSignalLocation.framework */,
+ 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */,
+ F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */,
+ 25848A5B7E93DCA744373200 /* OneSignal-Dynamic.framework */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 72F51C4EC858B862B629616C /* Services */ = {
+ isa = PBXGroup;
+ children = (
+ 0542854462194E28D9E4638D /* LiveActivityController.swift */,
+ 38138523A8A81A60A77800CA /* NotificationSender.swift */,
+ 5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */,
+ C76153AD2F6C6EB8F77138B9 /* PreferencesService.swift */,
+ D4EBF32FC1DBF3C34188E08C /* SecretsConfig.swift */,
+ 939E1F476EE48B9833A3311C /* TooltipService.swift */,
+ 3FD8258E807E6672642A32E6 /* UserFetchService.swift */,
+ );
+ path = Services;
+ sourceTree = "";
+ };
+ B9C3E998662065E7D921A5CA /* Components */ = {
+ isa = PBXGroup;
+ children = (
+ D6094499773760C3190F62C9 /* ActionButton.swift */,
+ D8D84B9C2B0EFE086A8E48CD /* AddItemDialog.swift */,
+ 2D959B1636916DAEE5FE6278 /* CustomNotificationDialog.swift */,
+ 5984B93007C6B85AFE09045A /* KeyValueRow.swift */,
+ 280B23B41935EAB89C8C6FCB /* ListWidgets.swift */,
+ 4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */,
+ D261D46C404E325CBA87A9E0 /* OSDialog.swift */,
+ D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */,
+ 3A654457BF0A55B54220E669 /* RemoveMultiDialog.swift */,
+ 497484E7C094D645338BD404 /* SectionCard.swift */,
+ F46DFACB9F304B9374F3C570 /* ToastView.swift */,
+ 911376C90AA43F41478596FE /* ToggleRow.swift */,
+ 38067B9DDA25E12809D9A245 /* TooltipDialog.swift */,
+ 432444EA41C495988DFAB422 /* TrackEventDialog.swift */,
+ );
+ path = Components;
+ sourceTree = "";
+ };
+ C83678AA9260FD8491BCC58F /* OneSignalWidget */ = {
+ isa = PBXGroup;
+ children = (
+ C360043C7B20683C2E3FA3B6 /* Assets.xcassets */,
+ 6DD14C3CEEDFB9557E589B45 /* OneSignalWidgetBundle.swift */,
+ 0864BD4A6F62539B2809338F /* OneSignalWidgetLiveActivity.swift */,
+ );
+ path = OneSignalWidget;
+ sourceTree = "";
+ };
+ E62E617DD5FF6C0AA303F74D /* OneSignalNotificationServiceExtension */ = {
+ isa = PBXGroup;
+ children = (
+ A20B46F63592FC67B655BEB8 /* NotificationService.swift */,
+ );
+ path = OneSignalNotificationServiceExtension;
+ sourceTree = "";
+ };
+ E81A0E25363045BA30E3D600 /* Projects */ = {
+ isa = PBXGroup;
+ children = (
+ 291D83300C20BA3831824AFD /* OneSignalSDK */,
+ );
+ name = Projects;
+ sourceTree = "";
+ };
+ E9C3DB3EC6D7655028A9C0D3 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 49D88A349FD70A9153DD2C03 /* App.app */,
+ B24E059F4DF6ABED55BA3183 /* OneSignalNotificationServiceExtension.appex */,
+ AB92C9C455B15E0B571A1C66 /* OneSignalWidget.appex */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ F0D08B397106EA8498C4A8F4 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ B9C3E998662065E7D921A5CA /* Components */,
+ 53EC9A9E14C3568A020C0977 /* Sections */,
+ 35F726E64F9B6817F917227C /* ContentView.swift */,
+ B3E7F504B0421F2B6247E2F5 /* Theme.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ "TEMP_9E448277-C21D-48E5-98E2-992293BCF68A" /* demo */ = {
+ isa = PBXGroup;
+ children = (
+ "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */,
+ );
+ path = demo;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 61033D7807F09753830EDBC1 /* OneSignalNotificationServiceExtension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = B4BE2C39FCD722539813E4EC /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */;
+ buildPhases = (
+ 5F10FF7EC3754C580B64A5B5 /* Sources */,
+ 83824651FEF6A3650CC5A580 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ D9BA8407D7B985A052FA30B8 /* PBXTargetDependency */,
+ 77C6618DD6ABFFC53745D2D4 /* PBXTargetDependency */,
+ E848F3B50C187ED7854A19EC /* PBXTargetDependency */,
+ );
+ name = OneSignalNotificationServiceExtension;
+ packageProductDependencies = (
+ );
+ productName = OneSignalNotificationServiceExtension;
+ productReference = B24E059F4DF6ABED55BA3183 /* OneSignalNotificationServiceExtension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+ 93E9E330FC2CE7458D9C925F /* App */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = FFC1863BD4026D2C17CBE82B /* Build configuration list for PBXNativeTarget "App" */;
+ buildPhases = (
+ B77F0AF073564580D75A2CF1 /* Sources */,
+ 37BFF6A8CB5CC7FA21AD3FA3 /* Resources */,
+ BC0DC3536DED64E0E8C9923E /* Frameworks */,
+ 7C6B2DB89D01A1E9B8F2A565 /* Embed Foundation Extensions */,
+ 8EFDE6013C818852098745E7 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 3D3214696D0E5D8871D927DC /* PBXTargetDependency */,
+ A1E826399F98FEF2C79AAD94 /* PBXTargetDependency */,
+ 39334E1C2B1194C7AA91290D /* PBXTargetDependency */,
+ E871C2D53DEBE7E6CD0D065C /* PBXTargetDependency */,
+ 1156917B5D40E6EEA429096E /* PBXTargetDependency */,
+ 4510858C7D7B77EA769F14BF /* PBXTargetDependency */,
+ D9EE246568FB5DB89C57EB72 /* PBXTargetDependency */,
+ DAE061F893FA97AAC97DC5B5 /* PBXTargetDependency */,
+ 6307D20A82A803B0C465DE8E /* PBXTargetDependency */,
+ 8438D2BC8BEECACED093384D /* PBXTargetDependency */,
+ 5CABB31140B2E97E15CB0194 /* PBXTargetDependency */,
+ 95E2CAB6F6B8EF265EC3BF95 /* PBXTargetDependency */,
+ );
+ name = App;
+ packageProductDependencies = (
+ );
+ productName = App;
+ productReference = 49D88A349FD70A9153DD2C03 /* App.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 97373B3889FBDD1762E98B03 /* OneSignalWidget */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = B4E4C9786F2CB642EDE86073 /* Build configuration list for PBXNativeTarget "OneSignalWidget" */;
+ buildPhases = (
+ FAFE195EF7AA226107A30633 /* Sources */,
+ 83BDEE3A221EC042F9FBEE81 /* Resources */,
+ C330DBFD3AE797742200EF8B /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ AADD02BB12D61D365C19EBE0 /* PBXTargetDependency */,
+ );
+ name = OneSignalWidget;
+ packageProductDependencies = (
+ );
+ productName = OneSignalWidget;
+ productReference = AB92C9C455B15E0B571A1C66 /* OneSignalWidget.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 6B590BF25178DC7D824D09CE /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1430;
+ TargetAttributes = {
+ };
+ };
+ buildConfigurationList = B3FD05C59F197F398A0B04AB /* Build configuration list for PBXProject "App" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ Base,
+ en,
+ );
+ mainGroup = 4102E3E068508DD683953C7D;
+ minimizedProjectReferenceProxies = 1;
+ preferredProjectObjectVersion = 77;
+ productRefGroup = E9C3DB3EC6D7655028A9C0D3 /* Products */;
+ projectDirPath = "";
+ projectReferences = (
+ {
+ ProductGroup = 54D70151146ED2430545164A /* Products */;
+ ProjectRef = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ },
+ );
+ projectRoot = "";
+ targets = (
+ 93E9E330FC2CE7458D9C925F /* App */,
+ 61033D7807F09753830EDBC1 /* OneSignalNotificationServiceExtension */,
+ 97373B3889FBDD1762E98B03 /* OneSignalWidget */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXReferenceProxy section */
+ 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalOSCore.framework;
+ remoteRef = 8B6EE756CF7175F38D5B5B4B /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 25848A5B7E93DCA744373200 /* OneSignal-Dynamic.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = "OneSignal-Dynamic.framework";
+ remoteRef = 25CC6D5CA1E9DA9142715E8A /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalNotifications.framework;
+ remoteRef = 852E98FC9C8818AE6B636551 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalCore.framework;
+ remoteRef = D399497AA4F221AD92C859AE /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalInAppMessages.framework;
+ remoteRef = C43833A058207841248D7506 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ B2F82BD207897EB78739182A /* OneSignalExtension.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalExtension.framework;
+ remoteRef = 06313C80EB92E867CF4AE55C /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalOutcomes.framework;
+ remoteRef = E36EAB44E3DD77FE80F2A724 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ EF49509A218369322ECFA3B9 /* OneSignalUser.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalUser.framework;
+ remoteRef = D47A29D1FE2C196E0715923B /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalLiveActivities.framework;
+ remoteRef = 8E39421BCBE5C21C1A9CB242 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ FE9834773C437CC373607693 /* OneSignalLocation.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalLocation.framework;
+ remoteRef = 996095BEB3FD8CE51B7F1AD7 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+/* End PBXReferenceProxy section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 37BFF6A8CB5CC7FA21AD3FA3 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3FB3C8C0634A765EE81D042E /* Assets.xcassets in Resources */,
+ 93104E0838915DD6194805D5 /* Secrets.plist in Resources */,
+ AB963AA6733C32EFB84DCBC0 /* vine_boom.wav in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 83BDEE3A221EC042F9FBEE81 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FB0472C73D007EABFD01CCA8 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 5F10FF7EC3754C580B64A5B5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5F4B7EC8437D1A8D80DF7674 /* NotificationService.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ B77F0AF073564580D75A2CF1 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9B0D1DD32B99602629BBCB95 /* ActionButton.swift in Sources */,
+ 1A247F505A707756873AA9FA /* AddItemDialog.swift in Sources */,
+ A7A5153D68A4967A88B2B433 /* AliasesSection.swift in Sources */,
+ A2B1975BB925DCA45327D71E /* App.swift in Sources */,
+ 6E3E040FD8A750248E70E46F /* AppModels.swift in Sources */,
+ F1DBBD7F18CB70C5DF2FFC32 /* AppSection.swift in Sources */,
+ 5B959D44AB09CB821C00AFBF /* ContentView.swift in Sources */,
+ 8EA2FF24D93691FDC1661913 /* CustomEventsSection.swift in Sources */,
+ 2859C7827BE28C7EE4AFDAB4 /* CustomNotificationDialog.swift in Sources */,
+ 0E7D0439A19C8BF291A8BEB1 /* EmailsSection.swift in Sources */,
+ 837FCE7A095ED1D7CCFEACF3 /* InAppSection.swift in Sources */,
+ E5671E3A84219D49E45F8DCF /* KeyValueRow.swift in Sources */,
+ 9A7BE456B679D7DE7CA300BC /* ListWidgets.swift in Sources */,
+ 7D2BA9022E77B00205453467 /* LiveActivityController.swift in Sources */,
+ DBDA727D6317A3CCC73A1699 /* LiveActivitySection.swift in Sources */,
+ B3AE701075398C6A369DBBE0 /* LocationSection.swift in Sources */,
+ E3725231A3FD5F5A88BAA758 /* MultiPairInputDialog.swift in Sources */,
+ 4C18E3D284BB28BD846162F3 /* NotificationSender.swift in Sources */,
+ 674995A7A55C13341317E19B /* OSDialog.swift in Sources */,
+ 12597AC14E1783CC87D6E147 /* OneSignalService.swift in Sources */,
+ 5737CABFA55E019B2732B90D /* OneSignalViewModel.swift in Sources */,
+ 39D2C94F79A62BFF9DE5DBA9 /* OutcomeDialog.swift in Sources */,
+ 14228D0C997F9168643F8154 /* OutcomesSection.swift in Sources */,
+ 20792A9930A201E187AA0ABF /* PreferencesService.swift in Sources */,
+ EA93E372AA3E66073487B89C /* PushSection.swift in Sources */,
+ 2FA8128D2A921DDF02210D8A /* RemoveMultiDialog.swift in Sources */,
+ 28D491D31B5C07E4D4F48A7D /* SecretsConfig.swift in Sources */,
+ 7B94F48C31E0BEA4B8CB20E2 /* SectionCard.swift in Sources */,
+ 27C72DF35BE082E3E1093F75 /* SendIamSection.swift in Sources */,
+ 0BC1978B56970258E20C24C4 /* SendPushSection.swift in Sources */,
+ 2FCC417641D480849E99588B /* SmsSection.swift in Sources */,
+ 3927A4BF207695E98A57E445 /* TagsSection.swift in Sources */,
+ 16B0C854BC2498BEEDC4EEED /* Theme.swift in Sources */,
+ 1F417CAD2528616703CAA54D /* ToastPresenter.swift in Sources */,
+ 7C904CE2F4C2A2083324BBEB /* ToastView.swift in Sources */,
+ 902A116B26B8ECAD8EE29C95 /* ToggleRow.swift in Sources */,
+ A2E180ADEA246FB6530E5C8C /* TooltipDialog.swift in Sources */,
+ 25C963476DF0B01BAABE24D9 /* TooltipService.swift in Sources */,
+ A8498B9A2AA7DA8CB0856CAC /* TrackEventDialog.swift in Sources */,
+ 1653658B1F4EE21203BBAA5D /* TriggersSection.swift in Sources */,
+ 0E6E9EEBF2500A4E70022960 /* UserFetchService.swift in Sources */,
+ CE2C54D552E42C2538FB95C4 /* UserSection.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ FAFE195EF7AA226107A30633 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 56DF63011CB625810F81075D /* OneSignalWidgetBundle.swift in Sources */,
+ 1FF9F70882A0CF6A73416DDF /* OneSignalWidgetLiveActivity.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 1156917B5D40E6EEA429096E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalUser;
+ targetProxy = 13BDB99578D2E19A9A750DEF /* PBXContainerItemProxy */;
+ };
+ 39334E1C2B1194C7AA91290D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalOutcomes;
+ targetProxy = 67BDC3D5140D6CC33418AA1D /* PBXContainerItemProxy */;
+ };
+ 3D3214696D0E5D8871D927DC /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalCore;
+ targetProxy = 9CA6E55F92E787A8186EEBA1 /* PBXContainerItemProxy */;
+ };
+ 4510858C7D7B77EA769F14BF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalExtension;
+ targetProxy = 8475704534409C59DAE8B29A /* PBXContainerItemProxy */;
+ };
+ 5CABB31140B2E97E15CB0194 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 61033D7807F09753830EDBC1 /* OneSignalNotificationServiceExtension */;
+ targetProxy = A668D6C94AC1F182AF01EAFF /* PBXContainerItemProxy */;
+ };
+ 6307D20A82A803B0C465DE8E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalLiveActivities;
+ targetProxy = E48045948F131DB2BD8C1B2A /* PBXContainerItemProxy */;
+ };
+ 77C6618DD6ABFFC53745D2D4 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalOutcomes;
+ targetProxy = 6701C77339EC73AE4FEC7E32 /* PBXContainerItemProxy */;
+ };
+ 8438D2BC8BEECACED093384D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalFramework;
+ targetProxy = 5C38CF82FCDF8C89EA732187 /* PBXContainerItemProxy */;
+ };
+ 95E2CAB6F6B8EF265EC3BF95 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97373B3889FBDD1762E98B03 /* OneSignalWidget */;
+ targetProxy = 642A3E9AC0D14239CE4A128E /* PBXContainerItemProxy */;
+ };
+ A1E826399F98FEF2C79AAD94 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalOSCore;
+ targetProxy = 6DE922175F2D1304874645CD /* PBXContainerItemProxy */;
+ };
+ AADD02BB12D61D365C19EBE0 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalLiveActivities;
+ targetProxy = D378765DB7FB67E222BB7FF1 /* PBXContainerItemProxy */;
+ };
+ D9BA8407D7B985A052FA30B8 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalCore;
+ targetProxy = B627D4859D5F7B2B7F452917 /* PBXContainerItemProxy */;
+ };
+ D9EE246568FB5DB89C57EB72 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalLocation;
+ targetProxy = 5C4A42186FB7137C13DD3C0D /* PBXContainerItemProxy */;
+ };
+ DAE061F893FA97AAC97DC5B5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalInAppMessages;
+ targetProxy = 005EC764DD2429AF6136BB9A /* PBXContainerItemProxy */;
+ };
+ E848F3B50C187ED7854A19EC /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalExtension;
+ targetProxy = 21265D9F7D2B1FBDDD3E716F /* PBXContainerItemProxy */;
+ };
+ E871C2D53DEBE7E6CD0D065C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalNotifications;
+ targetProxy = D1E066F0069BD9B244E196E4 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 0D2EF3911CA89837C30DB0D1 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ DEVELOPMENT_TEAM = 99SW8E36CT;
+ INFOPLIST_FILE = OneSignalWidget/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.LA;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 4A0C935808978B5A7673E412 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = App.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ DEVELOPMENT_TEAM = 99SW8E36CT;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ INFOPLIST_FILE = App/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 5C9EF0E6AF4F9491454DE177 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = "";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_INFOPLIST_FILE = NO;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ SWIFT_VERSION = 5.9;
+ };
+ name = Release;
+ };
+ D0E56A85F1C385808720F94B /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 99SW8E36CT;
+ INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.NSE;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Appium Demo - NSE";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ EB1CC3A930E09FEBECF9195D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = App.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 99SW8E36CT;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ INFOPLIST_FILE = App/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Appium Demo - Main";
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ F305A3E63851EE49DA2D190E /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = "";
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 99SW8E36CT;
+ INFOPLIST_FILE = OneSignalWidget/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.LA;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Appium Demo - Live Activity";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ F5FD25168D9B32A08A468069 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
+ DEVELOPMENT_TEAM = 99SW8E36CT;
+ INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.NSE;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ F61063B78755D98B1B9C3697 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "DEBUG=1",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_INFOPLIST_FILE = NO;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.9;
+ };
+ name = Debug;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ B3FD05C59F197F398A0B04AB /* Build configuration list for PBXProject "App" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F61063B78755D98B1B9C3697 /* Debug */,
+ 5C9EF0E6AF4F9491454DE177 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ B4BE2C39FCD722539813E4EC /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F5FD25168D9B32A08A468069 /* Debug */,
+ D0E56A85F1C385808720F94B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ B4E4C9786F2CB642EDE86073 /* Build configuration list for PBXNativeTarget "OneSignalWidget" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0D2EF3911CA89837C30DB0D1 /* Debug */,
+ F305A3E63851EE49DA2D190E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ FFC1863BD4026D2C17CBE82B /* Build configuration list for PBXNativeTarget "App" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 4A0C935808978B5A7673E412 /* Debug */,
+ EB1CC3A930E09FEBECF9195D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 6B590BF25178DC7D824D09CE /* Project object */;
+}
diff --git a/examples/demo/App/App.swift b/examples/demo/App/App.swift
new file mode 100644
index 000000000..58de08ea3
--- /dev/null
+++ b/examples/demo/App/App.swift
@@ -0,0 +1,140 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+import OneSignalFramework
+import OneSignalLiveActivities
+
+@main
+struct App: SwiftUI.App {
+ @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+ @StateObject private var viewModel = OneSignalViewModel()
+ @StateObject private var toastPresenter = ToastPresenter()
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ .environmentObject(viewModel)
+ .environmentObject(toastPresenter)
+ }
+ }
+}
+
+// MARK: - App Delegate
+
+class AppDelegate: NSObject, UIApplicationDelegate {
+
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
+ ) -> Bool {
+ // Initialize OneSignal
+ OneSignalService.shared.initialize(launchOptions: launchOptions)
+
+ // Set up notification lifecycle listeners
+ setupNotificationListeners()
+
+ // Set up in-app message listeners
+ setupInAppMessageListeners()
+
+ // Set up Live Activities (iOS 16.1+)
+ if #available(iOS 16.1, *) {
+ LiveActivityController.setup()
+ }
+
+ return true
+ }
+
+ private func setupNotificationListeners() {
+ // Foreground notification display
+ OneSignal.Notifications.addForegroundLifecycleListener(NotificationLifecycleHandler.shared)
+
+ // Notification click handling
+ OneSignal.Notifications.addClickListener(NotificationClickHandler.shared)
+ }
+
+ private func setupInAppMessageListeners() {
+ // In-app message lifecycle
+ OneSignal.InAppMessages.addLifecycleListener(InAppMessageLifecycleHandler.shared)
+
+ // In-app message click handling
+ OneSignal.InAppMessages.addClickListener(InAppMessageClickHandler.shared)
+ }
+}
+
+// MARK: - Notification Handlers
+
+class NotificationLifecycleHandler: NSObject, OSNotificationLifecycleListener {
+ static let shared = NotificationLifecycleHandler()
+
+ func onWillDisplay(event: OSNotificationWillDisplayEvent) {
+ print("[OneSignal] Notification will display: \(event.notification.title ?? "No title")")
+ // Optionally modify display behavior
+ // event.preventDefault() // Prevent automatic display
+ // event.notification.display() // Manually display later
+ }
+}
+
+class NotificationClickHandler: NSObject, OSNotificationClickListener {
+ static let shared = NotificationClickHandler()
+
+ func onClick(event: OSNotificationClickEvent) {
+ print("[OneSignal] Notification clicked: \(event.notification.title ?? "No title")")
+ // Handle notification click - navigate to specific screen, etc.
+ }
+}
+
+// MARK: - In-App Message Handlers
+
+class InAppMessageLifecycleHandler: NSObject, OSInAppMessageLifecycleListener {
+ static let shared = InAppMessageLifecycleHandler()
+
+ func onWillDisplay(event: OSInAppMessageWillDisplayEvent) {
+ print("[OneSignal] IAM will display: \(event.message.messageId)")
+ }
+
+ func onDidDisplay(event: OSInAppMessageDidDisplayEvent) {
+ print("[OneSignal] IAM did display: \(event.message.messageId)")
+ }
+
+ func onWillDismiss(event: OSInAppMessageWillDismissEvent) {
+ print("[OneSignal] IAM will dismiss: \(event.message.messageId)")
+ }
+
+ func onDidDismiss(event: OSInAppMessageDidDismissEvent) {
+ print("[OneSignal] IAM did dismiss: \(event.message.messageId)")
+ }
+}
+
+class InAppMessageClickHandler: NSObject, OSInAppMessageClickListener {
+ static let shared = InAppMessageClickHandler()
+
+ func onClick(event: OSInAppMessageClickEvent) {
+ print("[OneSignal] IAM clicked: \(event.result.actionId ?? "No action ID")")
+ // Handle IAM click - navigate, track event, etc.
+ }
+}
diff --git a/examples/demo/App/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/demo/App/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000..2c54006ed
--- /dev/null
+++ b/examples/demo/App/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x4D",
+ "green" : "0x4B",
+ "red" : "0xE5"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x6D",
+ "green" : "0x6B",
+ "red" : "0xF5"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/examples/demo/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/examples/demo/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png
new file mode 100644
index 000000000..a4d02a3bc
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ
diff --git a/examples/demo/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/demo/App/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..cefcc878e
--- /dev/null
+++ b/examples/demo/App/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images" : [
+ {
+ "filename" : "AppIcon.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/examples/demo/App/Assets.xcassets/Contents.json b/examples/demo/App/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..73c00596a
--- /dev/null
+++ b/examples/demo/App/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/examples/demo/App/Assets.xcassets/LaunchBackground.colorset/Contents.json b/examples/demo/App/Assets.xcassets/LaunchBackground.colorset/Contents.json
new file mode 100644
index 000000000..97650a1a6
--- /dev/null
+++ b/examples/demo/App/Assets.xcassets/LaunchBackground.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/Contents.json b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/Contents.json
new file mode 100644
index 000000000..f6b59d0ab
--- /dev/null
+++ b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "onesignal_launch_icon@1x.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "onesignal_launch_icon@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "onesignal_launch_icon@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@1x.png b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@1x.png
new file mode 100644
index 000000000..5898d09a6
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@1x.png differ
diff --git a/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@2x.png b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@2x.png
new file mode 100644
index 000000000..92cac76d9
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@2x.png differ
diff --git a/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@3x.png b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@3x.png
new file mode 100644
index 000000000..7f2c280df
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@3x.png differ
diff --git a/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/Contents.json b/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/Contents.json
new file mode 100644
index 000000000..43a089b72
--- /dev/null
+++ b/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "filename" : "onesignal_logo.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true,
+ "template-rendering-intent" : "original"
+ }
+}
diff --git a/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/onesignal_logo.pdf b/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/onesignal_logo.pdf
new file mode 100644
index 000000000..30d78ec1b
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/onesignal_logo.pdf differ
diff --git a/examples/demo/App/Info.plist b/examples/demo/App/Info.plist
new file mode 100644
index 000000000..21a2421ab
--- /dev/null
+++ b/examples/demo/App/Info.plist
@@ -0,0 +1,69 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ OneSignal Demo
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 2
+ LSRequiresIPhoneOS
+
+ NSLocationWhenInUseUsageDescription
+ This app uses your location to personalize notifications and content.
+ NSLocationAlwaysAndWhenInUseUsageDescription
+ This app uses your location to personalize notifications and content even when the app is in the background.
+ NSSupportsLiveActivities
+
+ NSSupportsLiveActivitiesFrequentUpdates
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+
+ UIBackgroundModes
+
+ remote-notification
+
+ UILaunchScreen
+
+ UIColorName
+ LaunchBackground
+ UIImageName
+ onesignal_launch_icon
+ UIImageRespectsSafeAreaInsets
+
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/examples/demo/App/Models/AppModels.swift b/examples/demo/App/Models/AppModels.swift
new file mode 100644
index 000000000..70ccd615a
--- /dev/null
+++ b/examples/demo/App/Models/AppModels.swift
@@ -0,0 +1,257 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+import UIKit
+
+// MARK: - Key-Value Item
+
+/// Generic key-value pair used for aliases, tags, and triggers
+struct KeyValueItem: Identifiable, Equatable {
+ let id = UUID()
+ let key: String
+ let value: String
+}
+
+// MARK: - Notification Type
+
+/// Push notification samples that can be sent from the demo
+enum NotificationType: String, CaseIterable, Identifiable {
+ case simple = "Simple"
+ case withImage = "With Image"
+ case withSound = "With Sound"
+
+ var id: String { rawValue }
+}
+
+// MARK: - In-App Message Type
+
+/// Sample in-app message layouts triggered by the iam_type trigger
+enum InAppMessageType: String, CaseIterable, Identifiable {
+ case topBanner = "Top Banner"
+ case bottomBanner = "Bottom Banner"
+ case centerModal = "Center Modal"
+ case fullScreen = "Full Screen"
+
+ var id: String { rawValue }
+
+ /// Trigger value the OneSignal IAM rules listen for
+ var triggerValue: String {
+ switch self {
+ case .topBanner: return "top_banner"
+ case .bottomBanner: return "bottom_banner"
+ case .centerModal: return "center_modal"
+ case .fullScreen: return "full_screen"
+ }
+ }
+}
+
+// MARK: - Add Item Type
+
+/// Single-input add dialog flavors
+enum AddItemType {
+ case alias
+ case email
+ case sms
+ case tag
+ case trigger
+ case externalUserId
+
+ var title: String {
+ switch self {
+ case .alias: return "Add Alias"
+ case .email: return "Add Email"
+ case .sms: return "Add SMS"
+ case .tag: return "Add Tag"
+ case .trigger: return "Add Trigger"
+ case .externalUserId: return "Login User"
+ }
+ }
+
+ var requiresKeyValue: Bool {
+ switch self {
+ case .alias, .tag, .trigger: return true
+ case .email, .sms, .externalUserId: return false
+ }
+ }
+
+ var keyPlaceholder: String {
+ switch self {
+ case .alias: return "Label"
+ case .tag, .trigger: return "Key"
+ default: return "Key"
+ }
+ }
+
+ var valuePlaceholder: String {
+ switch self {
+ case .alias: return "ID"
+ case .email: return "Email Address"
+ case .sms: return "Phone Number"
+ case .tag, .trigger: return "Value"
+ case .externalUserId: return "External User Id"
+ }
+ }
+
+ var keyboardType: UIKeyboardType {
+ switch self {
+ case .email: return .emailAddress
+ case .sms: return .phonePad
+ default: return .default
+ }
+ }
+
+ var confirmLabel: String {
+ switch self {
+ case .externalUserId: return "Login"
+ default: return "Add"
+ }
+ }
+
+ /// Stable accessibility id prefix shared with the rest of the demo
+ var accessibilityKey: String {
+ switch self {
+ case .alias: return "alias"
+ case .email: return "email"
+ case .sms: return "sms"
+ case .tag: return "tag"
+ case .trigger: return "trigger"
+ case .externalUserId: return "login_user_id"
+ }
+ }
+
+ /// Accessibility id for the first text field in two-input dialogs.
+ /// Mirrors the shared Appium spec naming (`alias_label_input`,
+ /// `tag_key_input`, `trigger_key_input`).
+ var keyInputID: String {
+ switch self {
+ case .alias: return "alias_label_input"
+ case .tag: return "tag_key_input"
+ case .trigger: return "trigger_key_input"
+ default: return "\(accessibilityKey)_key_input"
+ }
+ }
+
+ /// Accessibility id for the second / single text field.
+ /// Mirrors the shared Appium spec naming (`alias_id_input`,
+ /// `tag_value_input`, `trigger_value_input`, `email_input`,
+ /// `sms_input`, `login_user_id_input`).
+ var valueInputID: String {
+ switch self {
+ case .alias: return "alias_id_input"
+ case .tag: return "tag_value_input"
+ case .trigger: return "trigger_value_input"
+ default: return "\(accessibilityKey)_input"
+ }
+ }
+
+ /// Two-input flavors share `singlepair_*` buttons; single-input flavors
+ /// share `singleinput_*` so the Appium suite can find them by a stable id
+ /// regardless of the specific item type.
+ var confirmButtonID: String {
+ requiresKeyValue ? "singlepair_confirm_button" : "singleinput_confirm_button"
+ }
+
+ var cancelButtonID: String {
+ requiresKeyValue ? "singlepair_cancel_button" : "singleinput_cancel_button"
+ }
+}
+
+// MARK: - Multi-Add Item Type
+
+/// Multi-pair add dialog flavors (Add Multiple Aliases / Tags / Triggers)
+enum MultiAddItemType: String {
+ case aliases = "Add Multiple Aliases"
+ case tags = "Add Multiple Tags"
+ case triggers = "Add Multiple Triggers"
+
+ var keyPlaceholder: String {
+ switch self {
+ case .aliases: return "Label"
+ case .tags, .triggers: return "Key"
+ }
+ }
+
+ var valuePlaceholder: String {
+ switch self {
+ case .aliases: return "ID"
+ case .tags, .triggers: return "Value"
+ }
+ }
+}
+
+// MARK: - Remove Multi Item Type
+
+/// Multi-select remove dialog flavors
+enum RemoveMultiItemType: String {
+ case tags = "Remove Tags"
+ case triggers = "Remove Triggers"
+}
+
+// MARK: - Outcome Mode
+
+/// Variants supported by the Send Outcome dialog
+enum OutcomeMode: String, CaseIterable, Identifiable {
+ case normal = "Normal Outcome"
+ case unique = "Unique Outcome"
+ case value = "Outcome with Value"
+
+ var id: String { rawValue }
+
+ var accessibilityKey: String {
+ switch self {
+ case .normal: return "normal"
+ case .unique: return "unique"
+ case .value: return "value"
+ }
+ }
+}
+
+// MARK: - Tooltip Models
+
+/// Tooltip content fetched from sdk-shared (or bundled fallback)
+struct TooltipData {
+ let title: String
+ let description: String
+ let options: [TooltipOption]?
+}
+
+struct TooltipOption {
+ let name: String
+ let description: String
+}
+
+// MARK: - User Data
+
+/// User payload returned from the OneSignal /users API
+struct UserData {
+ let aliases: [String: String]
+ let tags: [String: String]
+ let emails: [String]
+ let smsNumbers: [String]
+ let externalId: String?
+}
diff --git a/examples/demo/App/Services/LiveActivityController.swift b/examples/demo/App/Services/LiveActivityController.swift
new file mode 100644
index 000000000..8d00a542d
--- /dev/null
+++ b/examples/demo/App/Services/LiveActivityController.swift
@@ -0,0 +1,153 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+import OneSignalFramework
+import OneSignalLiveActivities
+
+/// Order tracking phases used by the Live Activity demo
+enum LiveActivityStatus: String, CaseIterable, Identifiable {
+ case preparing
+ case onTheWay = "on_the_way"
+ case delivered
+
+ var id: String { rawValue }
+
+ var displayName: String {
+ switch self {
+ case .preparing: return "Preparing"
+ case .onTheWay: return "On The Way"
+ case .delivered: return "Delivered"
+ }
+ }
+
+ var message: String {
+ switch self {
+ case .preparing: return "Your order is being prepared"
+ case .onTheWay: return "Driver is heading your way"
+ case .delivered: return "Order delivered!"
+ }
+ }
+
+ var estimatedTime: String {
+ switch self {
+ case .preparing: return "15 min"
+ case .onTheWay: return "10 min"
+ case .delivered: return ""
+ }
+ }
+
+ /// Returns the next status in the preparing → on_the_way → delivered → preparing cycle
+ var next: LiveActivityStatus {
+ switch self {
+ case .preparing: return .onTheWay
+ case .onTheWay: return .delivered
+ case .delivered: return .preparing
+ }
+ }
+}
+
+/// Wraps the OneSignal Live Activities SDK and the REST API endpoints used to update / end activities
+enum LiveActivityController {
+
+ @available(iOS 16.1, *)
+ static func setup() {
+ OneSignal.LiveActivities.setupDefault()
+ }
+
+ @available(iOS 16.1, *)
+ static func start(
+ activityId: String,
+ orderNumber: String,
+ status: LiveActivityStatus
+ ) {
+ let attributes: [String: Any] = [
+ "orderNumber": orderNumber
+ ]
+ let content: [String: Any] = [
+ "status": status.rawValue,
+ "message": status.message,
+ "estimatedTime": status.estimatedTime
+ ]
+ OneSignal.LiveActivities.startDefault(
+ activityId,
+ attributes: attributes,
+ content: content
+ )
+ }
+
+ static func update(appId: String, activityId: String, status: LiveActivityStatus) async -> Bool {
+ let payload: [String: Any] = [
+ "event": "update",
+ "name": "Live Activity Update",
+ "priority": 10,
+ "event_updates": [
+ "data": [
+ "status": status.rawValue,
+ "message": status.message,
+ "estimatedTime": status.estimatedTime
+ ]
+ ]
+ ]
+ return await postLiveActivity(appId: appId, activityId: activityId, payload: payload)
+ }
+
+ static func end(appId: String, activityId: String) async -> Bool {
+ let payload: [String: Any] = [
+ "event": "end",
+ "name": "End Live Activity",
+ "priority": 10,
+ "dismissal_date": Int(Date().timeIntervalSince1970),
+ "event_updates": [
+ "message": "Ended Live Activity"
+ ]
+ ]
+ return await postLiveActivity(appId: appId, activityId: activityId, payload: payload)
+ }
+
+ static var hasApiKey: Bool { SecretsConfig.hasApiKey }
+
+ private static func postLiveActivity(appId: String, activityId: String, payload: [String: Any]) async -> Bool {
+ guard let key = SecretsConfig.apiKey else { return false }
+ let urlString = "https://api.onesignal.com/apps/\(appId)/live_activities/\(activityId)/notifications"
+ guard let url = URL(string: urlString) else { return false }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue("Key \(key)", forHTTPHeaderField: "Authorization")
+ request.httpBody = try? JSONSerialization.data(withJSONObject: payload, options: [])
+
+ do {
+ let (_, response) = try await URLSession.shared.data(for: request)
+ guard let http = response as? HTTPURLResponse else { return false }
+ return (200..<300).contains(http.statusCode)
+ } catch {
+ return false
+ }
+ }
+}
diff --git a/examples/demo/App/Services/NotificationSender.swift b/examples/demo/App/Services/NotificationSender.swift
new file mode 100644
index 000000000..f95b25739
--- /dev/null
+++ b/examples/demo/App/Services/NotificationSender.swift
@@ -0,0 +1,187 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+
+/// Posts to the OneSignal /notifications REST endpoint to send sample push payloads
+final class NotificationSender {
+ static let shared = NotificationSender()
+ private init() {}
+
+ enum SendError: Error, LocalizedError {
+ case noSubscriptionId
+ case requestFailed(String)
+ case transient(String)
+
+ var errorDescription: String? {
+ switch self {
+ case .noSubscriptionId: return "No push subscription"
+ case .requestFailed(let msg): return msg
+ case .transient(let msg): return msg
+ }
+ }
+ }
+
+ private let endpoint = URL(string: "https://onesignal.com/api/v1/notifications")!
+ private let maxAttempts = 5
+
+ func sendNotification(
+ _ type: NotificationType,
+ appId: String,
+ subscriptionId: String,
+ completion: @escaping (Result) -> Void
+ ) {
+ var headings = "Simple Notification"
+ var contents = "This is a simple push notification"
+ var extra: [String: Any] = [:]
+
+ switch type {
+ case .simple:
+ break
+ case .withImage:
+ headings = "Image Notification"
+ contents = "This notification includes an image"
+ let url = "https://media.onesignal.com/automated_push_templates/ratings_template.png"
+ extra["big_picture"] = url
+ extra["ios_attachments"] = ["image": url]
+ case .withSound:
+ headings = "Sound Notification"
+ contents = "This notification plays a custom sound"
+ extra["ios_sound"] = "vine_boom.wav"
+ }
+
+ post(
+ appId: appId,
+ subscriptionId: subscriptionId,
+ heading: headings,
+ content: contents,
+ extra: extra,
+ attempt: 1,
+ completion: completion
+ )
+ }
+
+ func sendCustomNotification(
+ title: String,
+ body: String,
+ appId: String,
+ subscriptionId: String,
+ completion: @escaping (Result) -> Void
+ ) {
+ post(
+ appId: appId,
+ subscriptionId: subscriptionId,
+ heading: title,
+ content: body,
+ extra: [:],
+ attempt: 1,
+ completion: completion
+ )
+ }
+
+ private func post(
+ appId: String,
+ subscriptionId: String,
+ heading: String,
+ content: String,
+ extra: [String: Any],
+ attempt: Int,
+ completion: @escaping (Result) -> Void
+ ) {
+ var payload: [String: Any] = [
+ "app_id": appId,
+ "include_subscription_ids": [subscriptionId],
+ "headings": ["en": heading],
+ "contents": ["en": content]
+ ]
+ payload.merge(extra) { _, new in new }
+
+ guard let body = try? JSONSerialization.data(withJSONObject: payload, options: []) else {
+ completion(.failure(SendError.requestFailed("Could not encode payload")))
+ return
+ }
+
+ var request = URLRequest(url: endpoint)
+ request.httpMethod = "POST"
+ request.httpBody = body
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue("application/vnd.onesignal.v1+json", forHTTPHeaderField: "Accept")
+
+ URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
+ guard let self = self else { return }
+ if let error = error {
+ completion(.failure(SendError.requestFailed(error.localizedDescription)))
+ return
+ }
+ guard let http = response as? HTTPURLResponse else {
+ completion(.failure(SendError.requestFailed("Unexpected response")))
+ return
+ }
+ guard (200..<300).contains(http.statusCode) else {
+ let text = data.flatMap { String(data: $0, encoding: .utf8) } ?? "HTTP \(http.statusCode)"
+ completion(.failure(SendError.requestFailed(text)))
+ return
+ }
+
+ // Treat 200 with empty id / errors / zero recipients as a transient backend race
+ // (subscription not yet indexed) and retry with exponential backoff.
+ if let data = data,
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ self.isTransientFailure(json) {
+ if attempt < self.maxAttempts {
+ let delay = UInt64(2_000_000_000) * UInt64(1 << (attempt - 1))
+ Task {
+ try? await Task.sleep(nanoseconds: delay)
+ self.post(
+ appId: appId,
+ subscriptionId: subscriptionId,
+ heading: heading,
+ content: content,
+ extra: extra,
+ attempt: attempt + 1,
+ completion: completion
+ )
+ }
+ return
+ }
+ completion(.failure(SendError.transient(String(describing: json))))
+ return
+ }
+
+ completion(.success(()))
+ }.resume()
+ }
+
+ private func isTransientFailure(_ json: [String: Any]) -> Bool {
+ let id = json["id"] as? String ?? ""
+ if id.isEmpty { return true }
+ if let recipients = json["recipients"] as? Int, recipients == 0 { return true }
+ if let errorsDict = json["errors"] as? [String: Any], !errorsDict.isEmpty { return true }
+ if let errorsArr = json["errors"] as? [Any], !errorsArr.isEmpty { return true }
+ return false
+ }
+}
diff --git a/examples/demo/App/Services/OneSignalService.swift b/examples/demo/App/Services/OneSignalService.swift
new file mode 100644
index 000000000..598189ea0
--- /dev/null
+++ b/examples/demo/App/Services/OneSignalService.swift
@@ -0,0 +1,240 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+import OneSignalFramework
+
+/// Thin wrapper that funnels demo calls through a single OneSignal entry point.
+/// Caching for state we restore across cold launches lives in `PreferencesService`.
+final class OneSignalService {
+
+ static let shared = OneSignalService()
+
+ private let prefs: PreferencesService
+
+ private init(prefs: PreferencesService = .shared) {
+ self.prefs = prefs
+ }
+
+ // MARK: - App ID
+
+ /// Read once at init from `Secrets.plist` (or the hard-coded fallback) so
+ /// the running session uses a stable value even if the bundle changes.
+ let appId: String = SecretsConfig.appId
+
+ // MARK: - Initialization
+
+ /// Mirrors the Capacitor demo's `useOneSignal` init order: feed cached
+ /// consent into the SDK BEFORE `initialize`, then restore IAM-paused,
+ /// location-shared, and a previously-logged-in external user id once the
+ /// SDK is ready. Without this, toggles flip back to defaults on every
+ /// cold launch.
+ func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
+ OneSignal.Debug.setLogLevel(.LL_VERBOSE)
+
+ OneSignal.setConsentRequired(prefs.getConsentRequired())
+ OneSignal.setConsentGiven(prefs.getConsentGiven())
+
+ OneSignal.initialize(appId, withLaunchOptions: launchOptions)
+
+ OneSignal.InAppMessages.paused = prefs.getIamPaused()
+ OneSignal.Location.isShared = prefs.getLocationShared()
+
+ if let storedExternalId = prefs.getExternalUserId() {
+ OneSignal.login(storedExternalId)
+ }
+ }
+
+ // MARK: - Identity
+
+ var onesignalId: String? { OneSignal.User.onesignalId }
+ var externalId: String? { OneSignal.User.externalId }
+
+ // MARK: - Consent
+
+ /// Read-through cache. `set` writes the value to `PreferencesService` and
+ /// forwards to the SDK so the next cold launch can restore it.
+ var consentRequired: Bool {
+ get { prefs.getConsentRequired() }
+ set {
+ prefs.setConsentRequired(newValue)
+ OneSignal.setConsentRequired(newValue)
+ }
+ }
+
+ var consentGiven: Bool {
+ get { prefs.getConsentGiven() }
+ set {
+ prefs.setConsentGiven(newValue)
+ OneSignal.setConsentGiven(newValue)
+ }
+ }
+
+ // MARK: - User
+
+ func login(externalId: String) {
+ prefs.setExternalUserId(externalId)
+ OneSignal.login(externalId)
+ }
+
+ func logout() {
+ prefs.setExternalUserId(nil)
+ OneSignal.logout()
+ }
+
+ // MARK: - Aliases
+
+ func addAlias(label: String, id: String) { OneSignal.User.addAlias(label: label, id: id) }
+ func addAliases(_ aliases: [String: String]) { OneSignal.User.addAliases(aliases) }
+ func removeAlias(_ label: String) { OneSignal.User.removeAlias(label) }
+ func removeAliases(_ labels: [String]) { OneSignal.User.removeAliases(labels) }
+
+ // MARK: - Push Subscription
+
+ var pushSubscriptionId: String? { OneSignal.User.pushSubscription.id }
+ var isPushEnabled: Bool { OneSignal.User.pushSubscription.optedIn }
+ var hasNotificationPermission: Bool { OneSignal.Notifications.permission }
+
+ func optInPush() { OneSignal.User.pushSubscription.optIn() }
+ func optOutPush() { OneSignal.User.pushSubscription.optOut() }
+
+ func requestPushPermission(completion: @escaping (Bool) -> Void) {
+ OneSignal.Notifications.requestPermission({ accepted in
+ completion(accepted)
+ }, fallbackToSettings: true)
+ }
+
+ // MARK: - Email
+
+ func addEmail(_ email: String) { OneSignal.User.addEmail(email) }
+ func removeEmail(_ email: String) { OneSignal.User.removeEmail(email) }
+
+ // MARK: - SMS
+
+ func addSms(_ number: String) { OneSignal.User.addSms(number) }
+ func removeSms(_ number: String) { OneSignal.User.removeSms(number) }
+
+ // MARK: - Tags
+
+ func addTag(key: String, value: String) { OneSignal.User.addTag(key: key, value: value) }
+ func addTags(_ tags: [String: String]) { OneSignal.User.addTags(tags) }
+ func removeTag(_ key: String) { OneSignal.User.removeTag(key) }
+ func removeTags(_ keys: [String]) { OneSignal.User.removeTags(keys) }
+ func getTags() -> [String: String] { OneSignal.User.getTags() }
+
+ // MARK: - Outcomes
+
+ func sendOutcome(_ name: String) { OneSignal.Session.addOutcome(name) }
+ func sendOutcome(_ name: String, value: NSNumber) { OneSignal.Session.addOutcome(name, value) }
+ func sendUniqueOutcome(_ name: String) { OneSignal.Session.addUniqueOutcome(name) }
+
+ // MARK: - In-App Messages
+
+ var isInAppMessagesPaused: Bool {
+ get { prefs.getIamPaused() }
+ set {
+ prefs.setIamPaused(newValue)
+ OneSignal.InAppMessages.paused = newValue
+ }
+ }
+
+ func addTrigger(key: String, value: String) {
+ OneSignal.InAppMessages.addTrigger(key, withValue: value)
+ }
+
+ func addTriggers(_ triggers: [String: String]) {
+ OneSignal.InAppMessages.addTriggers(triggers)
+ }
+
+ func removeTrigger(_ key: String) {
+ OneSignal.InAppMessages.removeTrigger(key)
+ }
+
+ func removeTriggers(_ keys: [String]) {
+ OneSignal.InAppMessages.removeTriggers(keys)
+ }
+
+ func clearTriggers() {
+ OneSignal.InAppMessages.clearTriggers()
+ }
+
+ // MARK: - Location
+
+ var isLocationShared: Bool {
+ get { prefs.getLocationShared() }
+ set {
+ prefs.setLocationShared(newValue)
+ OneSignal.Location.isShared = newValue
+ }
+ }
+
+ func requestLocationPermission() {
+ OneSignal.Location.requestPermission()
+ }
+
+ // MARK: - Notifications
+
+ func clearAllNotifications() {
+ OneSignal.Notifications.clearAll()
+ }
+
+ // MARK: - Custom Events
+
+ func trackEvent(name: String, properties: [String: Any]?) {
+ OneSignal.User.trackEvent(name: name, properties: properties)
+ }
+
+ // MARK: - Observers
+
+ func addPushSubscriptionObserver(_ observer: OSPushSubscriptionObserver) {
+ OneSignal.User.pushSubscription.addObserver(observer)
+ }
+
+ func addUserObserver(_ observer: OSUserStateObserver) {
+ OneSignal.User.addObserver(observer)
+ }
+
+ func addPermissionObserver(_ observer: OSNotificationPermissionObserver) {
+ OneSignal.Notifications.addPermissionObserver(observer)
+ }
+
+ func addNotificationClickListener(_ listener: OSNotificationClickListener) {
+ OneSignal.Notifications.addClickListener(listener)
+ }
+
+ func addNotificationLifecycleListener(_ listener: OSNotificationLifecycleListener) {
+ OneSignal.Notifications.addForegroundLifecycleListener(listener)
+ }
+
+ func addInAppMessageClickListener(_ listener: OSInAppMessageClickListener) {
+ OneSignal.InAppMessages.addClickListener(listener)
+ }
+
+ func addInAppMessageLifecycleListener(_ listener: OSInAppMessageLifecycleListener) {
+ OneSignal.InAppMessages.addLifecycleListener(listener)
+ }
+}
diff --git a/examples/demo/App/Services/PreferencesService.swift b/examples/demo/App/Services/PreferencesService.swift
new file mode 100644
index 000000000..b055224f7
--- /dev/null
+++ b/examples/demo/App/Services/PreferencesService.swift
@@ -0,0 +1,86 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+
+/// `UserDefaults`-backed cache for state the demo restores across cold launches:
+/// consent flags, IAM paused, location shared, and the last-logged-in external
+/// user id. Mirrors the Capacitor demo's `PreferencesService` so the iOS demo
+/// re-feeds these into the SDK during initialization.
+final class PreferencesService {
+
+ static let shared = PreferencesService()
+
+ private let defaults: UserDefaults
+
+ private init(defaults: UserDefaults = .standard) {
+ self.defaults = defaults
+ }
+
+ private enum Key {
+ static let consentRequired = "onesignal.demo.consentRequired"
+ static let consentGiven = "onesignal.demo.consentGiven"
+ static let iamPaused = "onesignal.demo.iamPaused"
+ static let locationShared = "onesignal.demo.locationShared"
+ static let externalUserId = "onesignal.demo.externalUserId"
+ }
+
+ // MARK: - Consent
+
+ func getConsentRequired() -> Bool { defaults.bool(forKey: Key.consentRequired) }
+ func setConsentRequired(_ value: Bool) { defaults.set(value, forKey: Key.consentRequired) }
+
+ func getConsentGiven() -> Bool { defaults.bool(forKey: Key.consentGiven) }
+ func setConsentGiven(_ value: Bool) { defaults.set(value, forKey: Key.consentGiven) }
+
+ // MARK: - In-App Messages
+
+ func getIamPaused() -> Bool { defaults.bool(forKey: Key.iamPaused) }
+ func setIamPaused(_ value: Bool) { defaults.set(value, forKey: Key.iamPaused) }
+
+ // MARK: - Location
+
+ func getLocationShared() -> Bool { defaults.bool(forKey: Key.locationShared) }
+ func setLocationShared(_ value: Bool) { defaults.set(value, forKey: Key.locationShared) }
+
+ // MARK: - External user id
+
+ func getExternalUserId() -> String? {
+ guard let value = defaults.string(forKey: Key.externalUserId), !value.isEmpty else {
+ return nil
+ }
+ return value
+ }
+
+ func setExternalUserId(_ value: String?) {
+ if let value = value, !value.isEmpty {
+ defaults.set(value, forKey: Key.externalUserId)
+ } else {
+ defaults.removeObject(forKey: Key.externalUserId)
+ }
+ }
+}
diff --git a/examples/demo/App/Services/SecretsConfig.swift b/examples/demo/App/Services/SecretsConfig.swift
new file mode 100644
index 000000000..51bfedeb5
--- /dev/null
+++ b/examples/demo/App/Services/SecretsConfig.swift
@@ -0,0 +1,71 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+
+/// Single source of truth for the demo's OneSignal credentials. Mirrors the
+/// Capacitor demo's `.env` (`ONESIGNAL_APP_ID`, `ONESIGNAL_API_KEY`) but reads
+/// values from `Secrets.plist` bundled with the app — the iOS-idiomatic
+/// equivalent. Both keys are optional; consumers fall back to platform defaults
+/// when missing.
+enum SecretsConfig {
+
+ /// Hard-coded fallback when `ONESIGNAL_APP_ID` is missing or empty in
+ /// `Secrets.plist`. Matches the default in `sdk-shared/demo/build.md`.
+ static let defaultAppId = "77e32082-ea27-42e3-a898-c72e141824ef"
+
+ /// Resolved OneSignal App ID. Reads `ONESIGNAL_APP_ID` from `Secrets.plist`
+ /// and falls back to `defaultAppId` when missing or empty.
+ static var appId: String {
+ string(forKey: "ONESIGNAL_APP_ID") ?? defaultAppId
+ }
+
+ /// Resolved REST API key, if any. Required for Live Activity update/end.
+ static var apiKey: String? { string(forKey: "ONESIGNAL_API_KEY") }
+
+ /// Convenience used by the Live Activity section to disable update/end
+ /// buttons when no key is configured.
+ static var hasApiKey: Bool { apiKey != nil }
+
+ private static let cache: [String: Any] = {
+ guard
+ let url = Bundle.main.url(forResource: "Secrets", withExtension: "plist"),
+ let data = try? Data(contentsOf: url),
+ let plist = try? PropertyListSerialization.propertyList(
+ from: data, options: [], format: nil
+ ) as? [String: Any]
+ else {
+ return [:]
+ }
+ return plist
+ }()
+
+ private static func string(forKey key: String) -> String? {
+ guard let value = cache[key] as? String, !value.isEmpty else { return nil }
+ return value
+ }
+}
diff --git a/examples/demo/App/Services/TooltipService.swift b/examples/demo/App/Services/TooltipService.swift
new file mode 100644
index 000000000..b568e1b29
--- /dev/null
+++ b/examples/demo/App/Services/TooltipService.swift
@@ -0,0 +1,169 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+
+/// Loads tooltip content shared with the other OneSignal demo apps
+final class TooltipService {
+ static let shared = TooltipService()
+
+ private static let remoteURL = URL(
+ string: "https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/tooltip_content.json"
+ )!
+
+ private var cache: [String: TooltipData] = [:]
+ private var loaded = false
+
+ private init() {
+ cache = TooltipService.bundledFallback()
+ }
+
+ func loadIfNeeded() {
+ guard !loaded else { return }
+ loaded = true
+
+ Task.detached { [weak self] in
+ guard let self = self else { return }
+ guard let (data, response) = try? await URLSession.shared.data(from: TooltipService.remoteURL),
+ let http = response as? HTTPURLResponse,
+ (200..<300).contains(http.statusCode),
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ return
+ }
+ var parsed: [String: TooltipData] = [:]
+ for (key, value) in json {
+ guard let dict = value as? [String: Any],
+ let title = dict["title"] as? String,
+ let description = dict["description"] as? String else {
+ continue
+ }
+ let options: [TooltipOption]?
+ if let rawOptions = dict["options"] as? [[String: Any]] {
+ options = rawOptions.compactMap { entry -> TooltipOption? in
+ guard let name = entry["name"] as? String,
+ let optDescription = entry["description"] as? String else { return nil }
+ return TooltipOption(name: name, description: optDescription)
+ }
+ } else {
+ options = nil
+ }
+ parsed[key] = TooltipData(title: title, description: description, options: options)
+ }
+ await MainActor.run {
+ if !parsed.isEmpty {
+ self.cache = parsed
+ }
+ }
+ }
+ }
+
+ func tooltip(for key: String) -> TooltipData? {
+ cache[key]
+ }
+
+ /// Minimal fallback content (keys match the sdk-shared tooltip JSON) so info icons
+ /// still work without network. `app` and `user` are demo-only and not in sdk-shared.
+ private static func bundledFallback() -> [String: TooltipData] {
+ [
+ "app": TooltipData(
+ title: "App",
+ description: "Your OneSignal App ID and consent settings.",
+ options: nil
+ ),
+ "user": TooltipData(
+ title: "User",
+ description: "External User Id is your own identifier for the current user. Login/logout to associate the device with a user.",
+ options: nil
+ ),
+ "push": TooltipData(
+ title: "Push Subscription",
+ description: "The push subscription for this device. Enables push notifications, in-app messages, and Live Activities.",
+ options: nil
+ ),
+ "sendPushNotification": TooltipData(
+ title: "Send Push Notification",
+ description: "Test push notifications by sending them to this device via the OneSignal REST API.",
+ options: nil
+ ),
+ "inAppMessaging": TooltipData(
+ title: "In-App Messaging",
+ description: "Display targeted messages inside your app. Pause IAM display while testing.",
+ options: nil
+ ),
+ "sendInAppMessage": TooltipData(
+ title: "Send In-App Message",
+ description: "Adds an iam_type trigger that your dashboard IAM rules can listen for.",
+ options: nil
+ ),
+ "aliases": TooltipData(
+ title: "Aliases",
+ description: "Custom label/id pairs that let you reference users by your own identifiers.",
+ options: nil
+ ),
+ "emails": TooltipData(
+ title: "Email Subscriptions",
+ description: "Email addresses associated with this user.",
+ options: nil
+ ),
+ "sms": TooltipData(
+ title: "SMS Subscriptions",
+ description: "Phone numbers associated with this user.",
+ options: nil
+ ),
+ "tags": TooltipData(
+ title: "Tags",
+ description: "Key-value string pairs attached to the user for segmentation and personalization.",
+ options: nil
+ ),
+ "outcomes": TooltipData(
+ title: "Outcomes",
+ description: "Track user actions attributed to push notifications.",
+ options: nil
+ ),
+ "triggers": TooltipData(
+ title: "Triggers",
+ description: "Device-local key-value pairs that control when in-app messages display.",
+ options: nil
+ ),
+ "customEvents": TooltipData(
+ title: "Custom Events",
+ description: "Send custom events with optional properties to trigger Journeys.",
+ options: nil
+ ),
+ "location": TooltipData(
+ title: "Location",
+ description: "Share device location for location-based segmentation.",
+ options: nil
+ ),
+ "liveActivities": TooltipData(
+ title: "Live Activities",
+ description: "Display real-time updates on the iOS Lock Screen and Dynamic Island.",
+ options: nil
+ )
+ ]
+ }
+}
diff --git a/examples/demo/App/Services/UserFetchService.swift b/examples/demo/App/Services/UserFetchService.swift
new file mode 100644
index 000000000..836559eaa
--- /dev/null
+++ b/examples/demo/App/Services/UserFetchService.swift
@@ -0,0 +1,98 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+
+/// Reads the OneSignal /users API to hydrate aliases / tags / channels in the demo
+final class UserFetchService {
+ static let shared = UserFetchService()
+ private init() {}
+
+ func fetchUser(appId: String, onesignalId: String) async -> UserData? {
+ let urlString = "https://api.onesignal.com/apps/\(appId)/users/by/onesignal_id/\(onesignalId)"
+ guard let url = URL(string: urlString) else { return nil }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "GET"
+
+ do {
+ let (data, response) = try await URLSession.shared.data(for: request)
+ guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
+ return nil
+ }
+ guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ return nil
+ }
+ return parse(json)
+ } catch {
+ return nil
+ }
+ }
+
+ private func parse(_ json: [String: Any]) -> UserData {
+ let identity = json["identity"] as? [String: Any] ?? [:]
+ let properties = json["properties"] as? [String: Any] ?? [:]
+ let subscriptions = json["subscriptions"] as? [[String: Any]] ?? []
+
+ var aliases: [String: String] = [:]
+ for (key, value) in identity {
+ guard key != "external_id", key != "onesignal_id" else { continue }
+ if let stringValue = value as? String {
+ aliases[key] = stringValue
+ }
+ }
+
+ var tags: [String: String] = [:]
+ if let rawTags = properties["tags"] as? [String: Any] {
+ for (key, value) in rawTags {
+ if let stringValue = value as? String {
+ tags[key] = stringValue
+ }
+ }
+ }
+
+ var emails: [String] = []
+ var smsNumbers: [String] = []
+ for sub in subscriptions {
+ let type = sub["type"] as? String ?? ""
+ let token = sub["token"] as? String ?? ""
+ guard !token.isEmpty else { continue }
+ if type == "Email" { emails.append(token) }
+ if type == "SMS" { smsNumbers.append(token) }
+ }
+
+ let externalId = identity["external_id"] as? String
+
+ return UserData(
+ aliases: aliases,
+ tags: tags,
+ emails: emails,
+ smsNumbers: smsNumbers,
+ externalId: externalId
+ )
+ }
+}
diff --git a/examples/demo/App/ViewModels/OneSignalViewModel.swift b/examples/demo/App/ViewModels/OneSignalViewModel.swift
new file mode 100644
index 000000000..dfa23553a
--- /dev/null
+++ b/examples/demo/App/ViewModels/OneSignalViewModel.swift
@@ -0,0 +1,473 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+import Combine
+import OneSignalFramework
+
+/// ViewModel that backs every section of the demo
+@MainActor
+final class OneSignalViewModel: ObservableObject {
+
+ // MARK: - App / Consent
+
+ @Published var appId: String
+ @Published var consentRequired: Bool = false
+ @Published var consentGiven: Bool = false
+
+ // MARK: - Identity
+
+ @Published var externalUserId: String?
+ @Published var aliases: [KeyValueItem] = []
+
+ // MARK: - Push
+
+ @Published var pushSubscriptionId: String?
+ @Published var isPushEnabled: Bool = false
+ @Published var hasNotificationPermission: Bool = false
+
+ // MARK: - Channels
+
+ @Published var emails: [String] = []
+ @Published var smsNumbers: [String] = []
+
+ // MARK: - Tags / Triggers
+
+ @Published var tags: [KeyValueItem] = []
+ @Published var triggers: [KeyValueItem] = []
+
+ // MARK: - In-App / Location
+
+ @Published var isInAppMessagesPaused: Bool = false
+ @Published var isLocationShared: Bool = false
+
+ // MARK: - UI State
+
+ @Published var isLoading: Bool = false
+
+ @Published var activeTooltip: TooltipData?
+
+ // MARK: - Computed
+
+ var isLoggedIn: Bool {
+ guard let id = externalUserId else { return false }
+ return !id.isEmpty
+ }
+
+ var loginButtonTitle: String { isLoggedIn ? "SWITCH USER" : "LOGIN USER" }
+
+ // MARK: - Private
+
+ private let service: OneSignalService
+ private let prefs: PreferencesService
+ private var observers = Observers()
+
+ /// Monotonically incremented on every `fetchUserDataFromApi` call. The
+ /// value captured at entry guards the post-await write so a slow fetch
+ /// for an old `onesignalId` cannot overwrite a newer fetch's results.
+ private var requestSequence: UInt64 = 0
+
+ // MARK: - Init
+
+ init(service: OneSignalService = .shared, prefs: PreferencesService = .shared) {
+ self.service = service
+ self.prefs = prefs
+ self.appId = service.appId
+ self.consentRequired = service.consentRequired
+ self.consentGiven = service.consentGiven
+ self.externalUserId = service.externalId ?? prefs.getExternalUserId()
+ self.hasNotificationPermission = service.hasNotificationPermission
+ refreshState()
+ setupObservers()
+
+ TooltipService.shared.loadIfNeeded()
+
+ if service.onesignalId != nil {
+ Task { await fetchUserDataFromApi() }
+ }
+ }
+
+ // MARK: - State sync
+
+ func refreshState() {
+ pushSubscriptionId = service.pushSubscriptionId
+ isPushEnabled = service.isPushEnabled
+ isInAppMessagesPaused = service.isInAppMessagesPaused
+ isLocationShared = service.isLocationShared
+ hasNotificationPermission = service.hasNotificationPermission
+ externalUserId = service.externalId
+
+ let sdkTags = service.getTags()
+ tags = sdkTags.map { KeyValueItem(key: $0.key, value: $0.value) }
+ }
+
+ func fetchUserDataFromApi() async {
+ guard let onesignalId = service.onesignalId else { return }
+ requestSequence &+= 1
+ let captured = requestSequence
+ isLoading = true
+
+ let userData = await UserFetchService.shared.fetchUser(appId: appId, onesignalId: onesignalId)
+
+ // Drop the result if a newer fetch has started while this one was in flight.
+ guard captured == requestSequence else { return }
+
+ if let userData = userData {
+ aliases = userData.aliases.map { KeyValueItem(key: $0.key, value: $0.value) }
+ tags = userData.tags.map { KeyValueItem(key: $0.key, value: $0.value) }
+ emails = userData.emails
+ smsNumbers = userData.smsNumbers
+ if let extId = userData.externalId, !extId.isEmpty {
+ externalUserId = extId
+ }
+ }
+ isLoading = false
+ }
+
+ // MARK: - Consent
+
+ func setConsentRequired(_ required: Bool) {
+ consentRequired = required
+ service.consentRequired = required
+ if !required {
+ consentGiven = true
+ service.consentGiven = true
+ }
+ }
+
+ func setConsentGiven(_ granted: Bool) {
+ consentGiven = granted
+ service.consentGiven = granted
+ }
+
+ // MARK: - User
+
+ func login(externalId: String) {
+ let trimmed = externalId.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty else { return }
+ isLoading = true
+ service.login(externalId: trimmed)
+ externalUserId = trimmed
+ clearUserData()
+ }
+
+ func logout() {
+ service.logout()
+ externalUserId = nil
+ clearUserData()
+ }
+
+ private func clearUserData() {
+ aliases.removeAll()
+ emails.removeAll()
+ smsNumbers.removeAll()
+ tags.removeAll()
+ triggers.removeAll()
+ }
+
+ // MARK: - Aliases
+
+ func addAlias(label: String, id: String) {
+ service.addAlias(label: label, id: id)
+ aliases.removeAll { $0.key == label }
+ aliases.append(KeyValueItem(key: label, value: id))
+ }
+
+ func addAliases(_ pairs: [(String, String)]) {
+ let dict = Dictionary(pairs, uniquingKeysWith: { _, last in last })
+ service.addAliases(dict)
+ for (key, value) in pairs {
+ aliases.removeAll { $0.key == key }
+ aliases.append(KeyValueItem(key: key, value: value))
+ }
+ }
+
+ func removeAlias(_ item: KeyValueItem) {
+ service.removeAlias(item.key)
+ aliases.removeAll { $0.id == item.id }
+ }
+
+ // MARK: - Push
+
+ func setPushEnabled(_ enabled: Bool) {
+ if enabled {
+ service.optInPush()
+ isPushEnabled = true
+ } else {
+ service.optOutPush()
+ isPushEnabled = false
+ }
+ }
+
+ func promptPushPermission() {
+ service.requestPushPermission { [weak self] accepted in
+ Task { @MainActor in
+ self?.hasNotificationPermission = accepted
+ self?.isPushEnabled = accepted
+ }
+ }
+ }
+
+ // MARK: - Email
+
+ func addEmail(_ email: String) {
+ service.addEmail(email)
+ if !emails.contains(email) { emails.append(email) }
+ }
+
+ func removeEmail(_ email: String) {
+ service.removeEmail(email)
+ emails.removeAll { $0 == email }
+ }
+
+ // MARK: - SMS
+
+ func addSms(_ number: String) {
+ service.addSms(number)
+ if !smsNumbers.contains(number) { smsNumbers.append(number) }
+ }
+
+ func removeSms(_ number: String) {
+ service.removeSms(number)
+ smsNumbers.removeAll { $0 == number }
+ }
+
+ // MARK: - Tags
+
+ func addTag(key: String, value: String) {
+ service.addTag(key: key, value: value)
+ tags.removeAll { $0.key == key }
+ tags.append(KeyValueItem(key: key, value: value))
+ }
+
+ func addTags(_ pairs: [(String, String)]) {
+ let dict = Dictionary(pairs, uniquingKeysWith: { _, last in last })
+ service.addTags(dict)
+ for (key, value) in pairs {
+ tags.removeAll { $0.key == key }
+ tags.append(KeyValueItem(key: key, value: value))
+ }
+ }
+
+ func removeTag(_ item: KeyValueItem) {
+ service.removeTag(item.key)
+ tags.removeAll { $0.id == item.id }
+ }
+
+ func removeSelectedTags(_ keys: [String]) {
+ guard !keys.isEmpty else { return }
+ service.removeTags(keys)
+ tags.removeAll { keys.contains($0.key) }
+ }
+
+ // MARK: - Outcomes
+
+ func sendOutcome(_ name: String) {
+ service.sendOutcome(name)
+ print("[OneSignal] Outcome sent: \(name)")
+ }
+
+ func sendUniqueOutcome(_ name: String) {
+ service.sendUniqueOutcome(name)
+ print("[OneSignal] Unique outcome sent: \(name)")
+ }
+
+ func sendOutcome(_ name: String, value: Double) {
+ service.sendOutcome(name, value: NSNumber(value: value))
+ print("[OneSignal] Outcome sent: \(name) = \(value)")
+ }
+
+ // MARK: - In-App
+
+ func setIamPaused(_ paused: Bool) {
+ isInAppMessagesPaused = paused
+ service.isInAppMessagesPaused = paused
+ }
+
+ func sendIamTrigger(_ type: InAppMessageType) {
+ service.addTrigger(key: "iam_type", value: type.triggerValue)
+ triggers.removeAll { $0.key == "iam_type" }
+ triggers.append(KeyValueItem(key: "iam_type", value: type.triggerValue))
+ }
+
+ // MARK: - Triggers
+
+ func addTrigger(key: String, value: String) {
+ service.addTrigger(key: key, value: value)
+ triggers.removeAll { $0.key == key }
+ triggers.append(KeyValueItem(key: key, value: value))
+ }
+
+ func addTriggers(_ pairs: [(String, String)]) {
+ let dict = Dictionary(pairs, uniquingKeysWith: { _, last in last })
+ service.addTriggers(dict)
+ for (key, value) in pairs {
+ triggers.removeAll { $0.key == key }
+ triggers.append(KeyValueItem(key: key, value: value))
+ }
+ }
+
+ func removeTrigger(_ item: KeyValueItem) {
+ service.removeTrigger(item.key)
+ triggers.removeAll { $0.id == item.id }
+ }
+
+ func removeSelectedTriggers(_ keys: [String]) {
+ guard !keys.isEmpty else { return }
+ service.removeTriggers(keys)
+ triggers.removeAll { keys.contains($0.key) }
+ }
+
+ func clearTriggers() {
+ service.clearTriggers()
+ triggers.removeAll()
+ }
+
+ // MARK: - Custom Events
+
+ func trackEvent(name: String, properties: [String: Any]?) {
+ service.trackEvent(name: name, properties: properties)
+ print("[OneSignal] Event tracked: \(name)")
+ }
+
+ // MARK: - Location
+
+ func setLocationShared(_ shared: Bool) {
+ isLocationShared = shared
+ service.isLocationShared = shared
+ }
+
+ func promptLocation() {
+ service.requestLocationPermission()
+ }
+
+ func checkLocationShared() -> Bool {
+ let shared = service.isLocationShared
+ print("[OneSignal] Location shared: \(shared)")
+ return shared
+ }
+
+ // MARK: - Notifications
+
+ func clearAllNotifications() {
+ service.clearAllNotifications()
+ }
+
+ func sendNotification(_ type: NotificationType) {
+ guard let subscriptionId = service.pushSubscriptionId, !subscriptionId.isEmpty else { return }
+ NotificationSender.shared.sendNotification(type, appId: appId, subscriptionId: subscriptionId) { _ in }
+ }
+
+ func sendCustomNotification(title: String, body: String) {
+ guard let subscriptionId = service.pushSubscriptionId, !subscriptionId.isEmpty else { return }
+ NotificationSender.shared.sendCustomNotification(title: title, body: body, appId: appId, subscriptionId: subscriptionId) { _ in }
+ }
+
+ // MARK: - Live Activities
+
+ func startLiveActivity(activityId: String, orderNumber: String, status: LiveActivityStatus) {
+ let trimmedId = activityId.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedId.isEmpty else { return }
+ if #available(iOS 16.1, *) {
+ LiveActivityController.start(
+ activityId: trimmedId,
+ orderNumber: orderNumber,
+ status: status
+ )
+ }
+ }
+
+ func updateLiveActivity(activityId: String, status: LiveActivityStatus) {
+ let trimmedId = activityId.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedId.isEmpty else { return }
+ Task {
+ _ = await LiveActivityController.update(
+ appId: appId,
+ activityId: trimmedId,
+ status: status
+ )
+ }
+ }
+
+ func endLiveActivity(activityId: String) {
+ let trimmedId = activityId.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedId.isEmpty else { return }
+ Task {
+ _ = await LiveActivityController.end(
+ appId: appId,
+ activityId: trimmedId
+ )
+ }
+ }
+
+ // MARK: - Tooltips
+
+ func showTooltip(for key: String) {
+ if let tooltip = TooltipService.shared.tooltip(for: key) {
+ activeTooltip = tooltip
+ }
+ }
+
+ func dismissTooltip() {
+ activeTooltip = nil
+ }
+
+ // MARK: - Observers
+
+ private func setupObservers() {
+ observers.viewModel = self
+ service.addPushSubscriptionObserver(observers)
+ service.addUserObserver(observers)
+ service.addPermissionObserver(observers)
+ }
+}
+
+// MARK: - Observer Bridge
+
+private final class Observers: NSObject, OSPushSubscriptionObserver, OSUserStateObserver, OSNotificationPermissionObserver {
+ weak var viewModel: OneSignalViewModel?
+
+ func onPushSubscriptionDidChange(state: OSPushSubscriptionChangedState) {
+ Task { @MainActor in
+ viewModel?.pushSubscriptionId = state.current.id
+ viewModel?.isPushEnabled = state.current.optedIn
+ }
+ }
+
+ func onUserStateDidChange(state: OSUserChangedState) {
+ Task { @MainActor in
+ await viewModel?.fetchUserDataFromApi()
+ }
+ }
+
+ func onNotificationPermissionDidChange(_ permission: Bool) {
+ Task { @MainActor in
+ viewModel?.hasNotificationPermission = permission
+ viewModel?.isPushEnabled = OneSignal.User.pushSubscription.optedIn
+ }
+ }
+}
diff --git a/examples/demo/App/ViewModels/ToastPresenter.swift b/examples/demo/App/ViewModels/ToastPresenter.swift
new file mode 100644
index 000000000..2b69f2efd
--- /dev/null
+++ b/examples/demo/App/ViewModels/ToastPresenter.swift
@@ -0,0 +1,56 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import Foundation
+import Combine
+
+/// UI-layer toast presenter per sdk-shared/demo/build.md Prompt 7.6.
+/// Feedback messages are owned by the UI layer (injected as an
+/// `@EnvironmentObject`), never by `OneSignalViewModel`. Replace-on-show:
+/// dismisses any visible toast and resets the [toastDurationMs] timer on
+/// every call.
+@MainActor
+final class ToastPresenter: ObservableObject {
+
+ static let toastDurationMs: UInt64 = 3_000
+
+ @Published var message: String?
+
+ private var dismissTask: Task?
+
+ func show(_ message: String) {
+ dismissTask?.cancel()
+ self.message = message
+ let target = message
+ dismissTask = Task { [weak self] in
+ try? await Task.sleep(nanoseconds: ToastPresenter.toastDurationMs * 1_000_000)
+ guard !Task.isCancelled else { return }
+ guard let self else { return }
+ if self.message == target { self.message = nil }
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Components/ActionButton.swift b/examples/demo/App/Views/Components/ActionButton.swift
new file mode 100644
index 000000000..a5a301642
--- /dev/null
+++ b/examples/demo/App/Views/Components/ActionButton.swift
@@ -0,0 +1,110 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Visual treatment of an action button. The spec defines exactly two variants:
+/// the filled primary, and the outlined ("destructive" / secondary) button.
+enum ActionButtonStyle {
+ case filled
+ case outline
+}
+
+/// Standard wide button used by sections.
+///
+/// Matches the spec: full width, 48 tall, 8 corner radius, semibold label,
+/// optional 18pt leading icon with 8pt gap before the label.
+struct ActionButton: View {
+ let title: String
+ let style: ActionButtonStyle
+ let icon: Image?
+ let isDisabled: Bool
+ let accessibilityID: String
+ let action: () -> Void
+
+ init(
+ _ title: String,
+ style: ActionButtonStyle = .filled,
+ icon: Image? = nil,
+ isDisabled: Bool = false,
+ accessibilityID: String,
+ action: @escaping () -> Void
+ ) {
+ self.title = title
+ self.style = style
+ self.icon = icon
+ self.isDisabled = isDisabled
+ self.accessibilityID = accessibilityID
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 8) {
+ if let icon = icon {
+ icon
+ .font(.system(size: OS.Layout.infoIconSize, weight: .semibold))
+ }
+ Text(title)
+ .font(OS.Font.bodyMedium.weight(.semibold))
+ .lineLimit(1)
+ }
+ .frame(maxWidth: .infinity)
+ .frame(height: OS.Layout.buttonHeight)
+ .foregroundColor(foregroundColor)
+ .background(backgroundColor)
+ .clipShape(RoundedRectangle(cornerRadius: OS.Radius.button))
+ .overlay(border)
+ }
+ .buttonStyle(.plain)
+ .disabled(isDisabled)
+ .opacity(isDisabled ? 0.5 : 1)
+ .accessibilityIdentifier(accessibilityID)
+ }
+
+ private var backgroundColor: Color {
+ switch style {
+ case .filled: return OS.Color.primary
+ case .outline: return .clear
+ }
+ }
+
+ private var foregroundColor: Color {
+ switch style {
+ case .filled: return .white
+ case .outline: return OS.Color.primary
+ }
+ }
+
+ @ViewBuilder
+ private var border: some View {
+ if case .outline = style {
+ RoundedRectangle(cornerRadius: OS.Radius.button)
+ .strokeBorder(OS.Color.primary, lineWidth: 1)
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Components/AddItemDialog.swift b/examples/demo/App/Views/Components/AddItemDialog.swift
new file mode 100644
index 000000000..f6b099578
--- /dev/null
+++ b/examples/demo/App/Views/Components/AddItemDialog.swift
@@ -0,0 +1,88 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Reusable centered dialog for adding items with one or two text fields.
+struct AddItemDialog: View {
+ let itemType: AddItemType
+ let onAdd: (String, String) -> Void
+ let onCancel: () -> Void
+
+ @State private var keyText: String = ""
+ @State private var valueText: String = ""
+
+ var body: some View {
+ OSDialog(
+ title: itemType.title,
+ confirmLabel: itemType.confirmLabel,
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: itemType.confirmButtonID,
+ cancelAccessibilityID: itemType.cancelButtonID,
+ onConfirm: {
+ onAdd(
+ keyText.trimmingCharacters(in: .whitespaces),
+ valueText.trimmingCharacters(in: .whitespaces)
+ )
+ },
+ onCancel: onCancel
+ ) {
+ VStack(spacing: 12) {
+ if itemType.requiresKeyValue {
+ HStack(spacing: 8) {
+ OSTextField(
+ placeholder: itemType.keyPlaceholder,
+ text: $keyText,
+ accessibilityID: itemType.keyInputID
+ )
+ OSTextField(
+ placeholder: itemType.valuePlaceholder,
+ text: $valueText,
+ keyboardType: itemType.keyboardType,
+ accessibilityID: itemType.valueInputID
+ )
+ }
+ } else {
+ OSTextField(
+ placeholder: itemType.valuePlaceholder,
+ text: $valueText,
+ keyboardType: itemType.keyboardType,
+ accessibilityID: itemType.valueInputID
+ )
+ }
+ }
+ }
+ }
+
+ private var isValid: Bool {
+ if itemType.requiresKeyValue {
+ return !keyText.trimmingCharacters(in: .whitespaces).isEmpty &&
+ !valueText.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+ return !valueText.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+}
diff --git a/examples/demo/App/Views/Components/CustomNotificationDialog.swift b/examples/demo/App/Views/Components/CustomNotificationDialog.swift
new file mode 100644
index 000000000..61df5e318
--- /dev/null
+++ b/examples/demo/App/Views/Components/CustomNotificationDialog.swift
@@ -0,0 +1,72 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Centered dialog for composing a custom push notification (title + body).
+struct CustomNotificationDialog: View {
+ let onSend: (String, String) -> Void
+ let onCancel: () -> Void
+
+ @State private var titleText: String = ""
+ @State private var bodyText: String = ""
+
+ var body: some View {
+ OSDialog(
+ title: "Custom Notification",
+ confirmLabel: "Send",
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: "custom_notification_send_button",
+ cancelAccessibilityID: "custom_notification_cancel_button",
+ onConfirm: {
+ onSend(
+ titleText.trimmingCharacters(in: .whitespaces),
+ bodyText.trimmingCharacters(in: .whitespaces)
+ )
+ },
+ onCancel: onCancel
+ ) {
+ VStack(spacing: 12) {
+ OSTextField(
+ placeholder: "Title",
+ text: $titleText,
+ accessibilityID: "custom_notification_title_input"
+ )
+ OSTextEditor(
+ placeholder: "Body",
+ text: $bodyText,
+ accessibilityID: "custom_notification_body_input"
+ )
+ }
+ }
+ }
+
+ private var isValid: Bool {
+ !titleText.trimmingCharacters(in: .whitespaces).isEmpty &&
+ !bodyText.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+}
diff --git a/examples/demo/App/Views/Components/KeyValueRow.swift b/examples/demo/App/Views/Components/KeyValueRow.swift
new file mode 100644
index 000000000..dd4c23a16
--- /dev/null
+++ b/examples/demo/App/Views/Components/KeyValueRow.swift
@@ -0,0 +1,59 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Single label/value horizontal row used inside the App / User / Push info cards.
+/// Label is 14pt, value is 12pt (monospaced when a value is an ID).
+struct InfoRow: View {
+ let label: String
+ let value: String
+ let valueAccessibilityID: String?
+ let isMonospaced: Bool
+
+ init(label: String, value: String, valueAccessibilityID: String? = nil, isMonospaced: Bool = false) {
+ self.label = label
+ self.value = value
+ self.valueAccessibilityID = valueAccessibilityID
+ self.isMonospaced = isMonospaced
+ }
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 12) {
+ Text(label)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ Text(value)
+ .font(isMonospaced ? OS.Font.mono12 : OS.Font.bodySmall)
+ .foregroundColor(OS.Color.bodyText)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .accessibilityIdentifier(valueAccessibilityID ?? "")
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Components/ListWidgets.swift b/examples/demo/App/Views/Components/ListWidgets.swift
new file mode 100644
index 000000000..eeb907f10
--- /dev/null
+++ b/examples/demo/App/Views/Components/ListWidgets.swift
@@ -0,0 +1,241 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+// MARK: - Shared list-card chrome
+
+private struct ListCardEmpty: View {
+ let text: String
+ let accessibilityID: String
+
+ var body: some View {
+ Text(text)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, OS.Spacing.cardPadding)
+ .accessibilityIdentifier(accessibilityID)
+ .osCard()
+ }
+}
+
+private struct ItemDivider: View {
+ var body: some View {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ }
+}
+
+private struct DeleteButton: View {
+ let accessibilityID: String
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Image(systemName: "xmark")
+ .font(.system(size: OS.Layout.infoIconSize, weight: .semibold))
+ .foregroundColor(OS.Color.primary)
+ .frame(width: 28, height: 28)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
+
+private struct MoreLink: View {
+ let hidden: Int
+ let onExpand: () -> Void
+ let accessibilityID: String
+
+ var body: some View {
+ Button(action: onExpand) {
+ Text("\(hidden) more")
+ .font(OS.Font.bodyMedium.weight(.medium))
+ .foregroundColor(OS.Color.primary)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
+
+// MARK: - Stacked (key-value) list
+
+/// List of paired items. Each row shows a 14pt key on top and a 12pt grey value below,
+/// with an optional close button to remove. Lists longer than `maxVisible` collapse
+/// into a "N more" link.
+struct PairList: View {
+ let items: [KeyValueItem]
+ let emptyText: String
+ let sectionKey: String
+ let onRemove: ((String) -> Void)?
+ let maxVisible: Int
+
+ @State private var expanded = false
+
+ init(
+ items: [KeyValueItem],
+ emptyText: String,
+ sectionKey: String,
+ onRemove: ((String) -> Void)? = nil,
+ maxVisible: Int = OS.Layout.listMaxVisible
+ ) {
+ self.items = items
+ self.emptyText = emptyText
+ self.sectionKey = sectionKey
+ self.onRemove = onRemove
+ self.maxVisible = maxVisible
+ }
+
+ private var visibleItems: [KeyValueItem] {
+ expanded ? items : Array(items.prefix(maxVisible))
+ }
+
+ private var hiddenCount: Int { max(0, items.count - maxVisible) }
+
+ var body: some View {
+ if items.isEmpty {
+ ListCardEmpty(text: emptyText, accessibilityID: "\(sectionKey)_empty")
+ } else {
+ VStack(spacing: 0) {
+ ForEach(visibleItems.indices, id: \.self) { index in
+ let item = visibleItems[index]
+ HStack(alignment: .center, spacing: 8) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(item.key)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .accessibilityIdentifier("\(sectionKey)_pair_key_\(item.key)")
+ Text(item.value)
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+ .accessibilityIdentifier("\(sectionKey)_pair_value_\(item.key)")
+ }
+ Spacer(minLength: 0)
+ if let onRemove = onRemove {
+ DeleteButton(
+ accessibilityID: "\(sectionKey)_remove_\(item.key)",
+ action: { onRemove(item.key) }
+ )
+ }
+ }
+ .padding(.vertical, 4)
+ .padding(.horizontal, 4)
+
+ if index < visibleItems.count - 1 {
+ ItemDivider()
+ }
+ }
+ if !expanded && hiddenCount > 0 {
+ ItemDivider()
+ MoreLink(
+ hidden: hiddenCount,
+ onExpand: { expanded = true },
+ accessibilityID: "\(sectionKey)_more"
+ )
+ }
+ }
+ .osCard()
+ }
+ }
+}
+
+// MARK: - Unstacked (single-string) list
+
+/// List of plain string items (emails, sms numbers). Single 14pt line per row.
+struct SingleList: View {
+ let items: [String]
+ let emptyText: String
+ let sectionKey: String
+ let onRemove: ((String) -> Void)?
+ let maxVisible: Int
+
+ @State private var expanded = false
+
+ init(
+ items: [String],
+ emptyText: String,
+ sectionKey: String,
+ onRemove: ((String) -> Void)? = nil,
+ maxVisible: Int = OS.Layout.listMaxVisible
+ ) {
+ self.items = items
+ self.emptyText = emptyText
+ self.sectionKey = sectionKey
+ self.onRemove = onRemove
+ self.maxVisible = maxVisible
+ }
+
+ private var visibleItems: [String] {
+ expanded ? items : Array(items.prefix(maxVisible))
+ }
+
+ private var hiddenCount: Int { max(0, items.count - maxVisible) }
+
+ var body: some View {
+ if items.isEmpty {
+ ListCardEmpty(text: emptyText, accessibilityID: "\(sectionKey)_empty")
+ } else {
+ VStack(spacing: 0) {
+ ForEach(visibleItems.indices, id: \.self) { index in
+ let item = visibleItems[index]
+ HStack(alignment: .center, spacing: 8) {
+ Text(item)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .accessibilityIdentifier("\(sectionKey)_value_\(item)")
+ Spacer(minLength: 0)
+ if let onRemove = onRemove {
+ DeleteButton(
+ accessibilityID: "\(sectionKey)_remove_\(item)",
+ action: { onRemove(item) }
+ )
+ }
+ }
+ .padding(.vertical, 4)
+ .padding(.horizontal, 4)
+
+ if index < visibleItems.count - 1 {
+ ItemDivider()
+ }
+ }
+ if !expanded && hiddenCount > 0 {
+ ItemDivider()
+ MoreLink(
+ hidden: hiddenCount,
+ onExpand: { expanded = true },
+ accessibilityID: "\(sectionKey)_more"
+ )
+ }
+ }
+ .osCard()
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Components/MultiPairInputDialog.swift b/examples/demo/App/Views/Components/MultiPairInputDialog.swift
new file mode 100644
index 000000000..b785b7c3c
--- /dev/null
+++ b/examples/demo/App/Views/Components/MultiPairInputDialog.swift
@@ -0,0 +1,156 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Centered dialog that adds multiple key/value pairs at once
+/// (Add Multiple Aliases / Tags / Triggers).
+struct MultiPairInputDialog: View {
+ let type: MultiAddItemType
+ let onAdd: ([(String, String)]) -> Void
+ let onCancel: () -> Void
+
+ @State private var rows: [Row] = [Row()]
+ @State private var measuredContentHeight: CGFloat = 0
+
+ struct Row: Identifiable {
+ let id = UUID()
+ var key: String = ""
+ var value: String = ""
+ }
+
+ /// Upper bound for the rows ScrollView. Keeps the dialog from growing
+ /// past a sensible point on small devices; content scrolls beyond this.
+ private let maxRowsHeight: CGFloat = 320
+
+ var body: some View {
+ OSDialog(
+ title: type.rawValue,
+ confirmLabel: "Add All",
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: "multipair_confirm_button",
+ cancelAccessibilityID: "multipair_cancel_button",
+ onConfirm: {
+ let pairs = rows.compactMap { row -> (String, String)? in
+ let key = row.key.trimmingCharacters(in: .whitespaces)
+ let value = row.value.trimmingCharacters(in: .whitespaces)
+ guard !key.isEmpty, !value.isEmpty else { return nil }
+ return (key, value)
+ }
+ onAdd(pairs)
+ },
+ onCancel: onCancel
+ ) {
+ // Always wrap in a ScrollView so the SwiftUI view hierarchy stays
+ // stable as the keyboard appears. The earlier `ViewThatFits` swap
+ // (VStack ↔ ScrollView) tore down the focused TextField when the
+ // keyboard shrunk the safe area, dropping focus mid-typing — that
+ // broke Appium input on the second row's value field. The frame
+ // is sized to the measured rows height (clamped) so the dialog
+ // still grows/shrinks with row count instead of always claiming
+ // the maximum.
+ ScrollView {
+ rowsContent
+ .background(
+ GeometryReader { proxy in
+ Color.clear.preference(
+ key: RowsHeightPreferenceKey.self,
+ value: proxy.size.height
+ )
+ }
+ )
+ }
+ .frame(height: min(max(measuredContentHeight, 1), maxRowsHeight))
+ .onPreferenceChange(RowsHeightPreferenceKey.self) { measuredContentHeight = $0 }
+ }
+ }
+
+ private var rowsContent: some View {
+ VStack(spacing: 12) {
+ ForEach(rows.indices, id: \.self) { index in
+ VStack(spacing: 8) {
+ HStack(spacing: 8) {
+ OSTextField(
+ placeholder: type.keyPlaceholder,
+ text: $rows[index].key,
+ accessibilityID: "multipair_key_\(index)"
+ )
+ OSTextField(
+ placeholder: type.valuePlaceholder,
+ text: $rows[index].value,
+ accessibilityID: "multipair_value_\(index)"
+ )
+ if rows.count > 1 {
+ Button {
+ rows.remove(at: index)
+ } label: {
+ Image(systemName: "xmark")
+ .font(.system(size: OS.Layout.infoIconSize, weight: .semibold))
+ .foregroundColor(OS.Color.primary)
+ .frame(width: 28, height: 28)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier("multipair_remove_row_\(index)")
+ }
+ }
+ if index < rows.count - 1 {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ }
+ }
+ }
+
+ Button {
+ rows.append(Row())
+ } label: {
+ Text("+ Add")
+ .font(OS.Font.bodyMedium.weight(.bold))
+ .foregroundColor(OS.Color.primary)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier("multipair_add_row_button")
+ }
+ }
+
+ private var isValid: Bool {
+ guard !rows.isEmpty else { return false }
+ return rows.allSatisfy { row in
+ !row.key.trimmingCharacters(in: .whitespaces).isEmpty &&
+ !row.value.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+ }
+}
+
+private struct RowsHeightPreferenceKey: PreferenceKey {
+ static var defaultValue: CGFloat = 0
+ static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
+ value = max(value, nextValue())
+ }
+}
diff --git a/examples/demo/App/Views/Components/OSDialog.swift b/examples/demo/App/Views/Components/OSDialog.swift
new file mode 100644
index 000000000..c591dcb6a
--- /dev/null
+++ b/examples/demo/App/Views/Components/OSDialog.swift
@@ -0,0 +1,255 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+// MARK: - Dialog container
+
+/// Standard dialog body. Wraps the supplied content in a vertical stack with
+/// 24pt outer padding, places a 24pt-weight-regular title above it, and pins
+/// an action row (Cancel / confirm) to the bottom.
+struct OSDialog: View {
+ let title: String
+ let confirmLabel: String
+ let isConfirmEnabled: Bool
+ let confirmAccessibilityID: String
+ let cancelAccessibilityID: String
+ let onConfirm: () -> Void
+ let onCancel: () -> Void
+ @ViewBuilder let content: () -> Content
+
+ init(
+ title: String,
+ confirmLabel: String = "Save",
+ isConfirmEnabled: Bool = true,
+ confirmAccessibilityID: String = "dialog_confirm_button",
+ cancelAccessibilityID: String = "dialog_cancel_button",
+ onConfirm: @escaping () -> Void,
+ onCancel: @escaping () -> Void,
+ @ViewBuilder content: @escaping () -> Content
+ ) {
+ self.title = title
+ self.confirmLabel = confirmLabel
+ self.isConfirmEnabled = isConfirmEnabled
+ self.confirmAccessibilityID = confirmAccessibilityID
+ self.cancelAccessibilityID = cancelAccessibilityID
+ self.onConfirm = onConfirm
+ self.onCancel = onCancel
+ self.content = content
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text(title)
+ .font(.system(size: 24, weight: .regular))
+ .foregroundColor(OS.Color.bodyText)
+
+ content()
+
+ HStack(spacing: 8) {
+ Spacer(minLength: 0)
+ OSDialogActionButton(
+ title: "Cancel",
+ accessibilityID: cancelAccessibilityID,
+ isEnabled: true,
+ action: onCancel
+ )
+ OSDialogActionButton(
+ title: confirmLabel,
+ accessibilityID: confirmAccessibilityID,
+ isEnabled: isConfirmEnabled,
+ action: onConfirm
+ )
+ }
+ .padding(.top, 8)
+ }
+ .padding(24)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(OS.Color.cardBackground)
+ }
+}
+
+// MARK: - Dialog action button
+
+/// Text-style action button for dialog footers.
+/// Spec: 14pt, weight medium/500, color osPrimary, 12 horizontal / 8 vertical padding.
+struct OSDialogActionButton: View {
+ let title: String
+ let accessibilityID: String
+ let isEnabled: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Text(title)
+ .font(OS.Font.bodyMedium.weight(.medium))
+ .foregroundColor(isEnabled ? OS.Color.primary : OS.Color.grey500)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ }
+ .buttonStyle(.plain)
+ .disabled(!isEnabled)
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
+
+// MARK: - Dialog text inputs
+
+/// Bordered text field used inside dialogs. Spec: 8 corner radius,
+/// 12 horizontal / 14 vertical content padding, 1px solid grey700 border,
+/// 2px solid osPrimary on focus.
+struct OSTextField: View {
+ let placeholder: String
+ @Binding var text: String
+ var keyboardType: UIKeyboardType = .default
+ var autocorrect: Bool = false
+ var capitalization: TextInputAutocapitalization = .never
+ var accessibilityID: String
+
+ @FocusState private var focused: Bool
+
+ var body: some View {
+ TextField(placeholder, text: $text)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .keyboardType(keyboardType)
+ .textInputAutocapitalization(capitalization)
+ .autocorrectionDisabled(!autocorrect)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 14)
+ .focused($focused)
+ .background(
+ RoundedRectangle(cornerRadius: OS.Radius.input)
+ .strokeBorder(
+ focused ? OS.Color.primary : OS.Color.grey700,
+ lineWidth: focused ? 2 : 1
+ )
+ )
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
+
+/// Bordered multi-line text editor mirroring `OSTextField`'s visual.
+struct OSTextEditor: View {
+ let placeholder: String
+ @Binding var text: String
+ var minHeight: CGFloat = 90
+ var accessibilityID: String
+
+ @FocusState private var focused: Bool
+
+ var body: some View {
+ ZStack(alignment: .topLeading) {
+ if text.isEmpty {
+ Text(placeholder)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 18)
+ .allowsHitTesting(false)
+ }
+
+ TextEditor(text: $text)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .scrollContentBackground(.hidden)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 8)
+ .focused($focused)
+ .frame(minHeight: minHeight)
+ .accessibilityIdentifier(accessibilityID)
+ }
+ .background(
+ RoundedRectangle(cornerRadius: OS.Radius.input)
+ .strokeBorder(
+ focused ? OS.Color.primary : OS.Color.grey700,
+ lineWidth: focused ? 2 : 1
+ )
+ )
+ }
+}
+
+// MARK: - Centered dialog presentation
+
+extension View {
+ /// Presents `content` as a centered modal dialog over the entire screen,
+ /// matching the styles.md "Dialogs" spec: 54% black backdrop, 16pt
+ /// horizontal / 24pt vertical insets, 28pt corner radius, white card.
+ /// Tapping the backdrop dismisses.
+ ///
+ /// Uses `.fullScreenCover` rather than `.overlay` so the backdrop and
+ /// dialog are anchored to the window — sections can attach this modifier
+ /// without the overlay being clipped to the section's frame inside
+ /// `ScrollView`. The cover's UIHostingController background is forced
+ /// clear via `ClearBackgroundView` so the dialog's own backdrop is what
+ /// the user sees (iOS 16.4+ has `presentationBackground(.clear)`; this
+ /// works on the demo's 16.0 deployment target).
+ func osCenteredDialog(
+ isPresented: Binding,
+ @ViewBuilder content: @escaping () -> DialogContent
+ ) -> some View {
+ modifier(OSCenteredDialogModifier(isPresented: isPresented, dialog: content))
+ }
+}
+
+private struct OSCenteredDialogModifier: ViewModifier {
+ @Binding var isPresented: Bool
+ @ViewBuilder var dialog: () -> DialogContent
+
+ func body(content: Content) -> some View {
+ content.fullScreenCover(isPresented: $isPresented) {
+ ZStack {
+ OS.Color.backdrop
+ .ignoresSafeArea()
+ .contentShape(Rectangle())
+ .onTapGesture { isPresented = false }
+
+ dialog()
+ .clipShape(RoundedRectangle(cornerRadius: OS.Radius.modal))
+ .padding(.horizontal, 16)
+ .padding(.vertical, 24)
+ }
+ .background(ClearBackgroundView())
+ }
+ .transaction { $0.disablesAnimations = true }
+ }
+}
+
+/// UIKit bridge that walks up to the `UIHostingController`'s view and clears
+/// its background so the SwiftUI `fullScreenCover` is see-through. Required on
+/// iOS < 16.4 where `presentationBackground(.clear)` is unavailable.
+private struct ClearBackgroundView: UIViewRepresentable {
+ func makeUIView(context: Context) -> UIView {
+ let view = UIView()
+ DispatchQueue.main.async {
+ view.superview?.superview?.backgroundColor = .clear
+ }
+ return view
+ }
+
+ func updateUIView(_ uiView: UIView, context: Context) {}
+}
diff --git a/examples/demo/App/Views/Components/OutcomeDialog.swift b/examples/demo/App/Views/Components/OutcomeDialog.swift
new file mode 100644
index 000000000..23493d36c
--- /dev/null
+++ b/examples/demo/App/Views/Components/OutcomeDialog.swift
@@ -0,0 +1,112 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Centered dialog for sending an outcome (normal, unique, or with value).
+struct OutcomeDialog: View {
+ let onSend: (String, OutcomeMode, Double?) -> Void
+ let onCancel: () -> Void
+
+ @State private var mode: OutcomeMode = .normal
+ @State private var name: String = ""
+ @State private var valueText: String = ""
+
+ var body: some View {
+ OSDialog(
+ title: "Send Outcome",
+ confirmLabel: "Send",
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: "outcome_send_button",
+ cancelAccessibilityID: "outcome_cancel_button",
+ onConfirm: {
+ let trimmed = name.trimmingCharacters(in: .whitespaces)
+ let value: Double? = mode == .value ? Double(valueText) : nil
+ onSend(trimmed, mode, value)
+ },
+ onCancel: onCancel
+ ) {
+ VStack(spacing: 14) {
+ ForEach(OutcomeMode.allCases) { option in
+ OutcomeRadioRow(
+ title: option.rawValue,
+ isSelected: mode == option,
+ accessibilityID: "outcome_type_\(option.accessibilityKey)_radio",
+ onTap: { mode = option }
+ )
+ }
+
+ OSTextField(
+ placeholder: "Outcome Name",
+ text: $name,
+ accessibilityID: "outcome_name_input"
+ )
+
+ if mode == .value {
+ OSTextField(
+ placeholder: "Outcome Value",
+ text: $valueText,
+ keyboardType: .decimalPad,
+ accessibilityID: "outcome_value_input"
+ )
+ }
+ }
+ }
+ }
+
+ private var isValid: Bool {
+ let trimmedName = name.trimmingCharacters(in: .whitespaces)
+ guard !trimmedName.isEmpty else { return false }
+ if mode == .value {
+ return Double(valueText) != nil
+ }
+ return true
+ }
+}
+
+private struct OutcomeRadioRow: View {
+ let title: String
+ let isSelected: Bool
+ let accessibilityID: String
+ let onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ HStack(alignment: .center, spacing: 12) {
+ Image(systemName: isSelected ? "largecircle.fill.circle" : "circle")
+ .font(.system(size: 24))
+ .foregroundColor(isSelected ? OS.Color.primary : OS.Color.grey700)
+ Text(title)
+ .font(OS.Font.bodyLarge)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ }
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
diff --git a/examples/demo/App/Views/Components/RemoveMultiDialog.swift b/examples/demo/App/Views/Components/RemoveMultiDialog.swift
new file mode 100644
index 000000000..53897fae5
--- /dev/null
+++ b/examples/demo/App/Views/Components/RemoveMultiDialog.swift
@@ -0,0 +1,142 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Centered dialog that lets the user pick multiple keys to remove
+/// (Remove Tags / Remove Triggers).
+struct RemoveMultiDialog: View {
+ let type: RemoveMultiItemType
+ let items: [KeyValueItem]
+ let onRemove: ([String]) -> Void
+ let onCancel: () -> Void
+
+ @State private var selected: Set = []
+ @State private var measuredContentHeight: CGFloat = 0
+
+ /// Upper bound for the rows ScrollView. Content scrolls beyond this.
+ private let maxRowsHeight: CGFloat = 320
+
+ var body: some View {
+ OSDialog(
+ title: type.rawValue,
+ confirmLabel: selected.isEmpty ? "Remove" : "Remove (\(selected.count))",
+ isConfirmEnabled: !selected.isEmpty,
+ confirmAccessibilityID: "multiselect_confirm_button",
+ cancelAccessibilityID: "multiselect_cancel_button",
+ onConfirm: { onRemove(Array(selected)) },
+ onCancel: onCancel
+ ) {
+ if items.isEmpty {
+ Text("Nothing to remove")
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, OS.Spacing.cardPadding)
+ .accessibilityIdentifier("remove_multi_empty")
+ } else {
+ // Always wrap in a ScrollView (single stable view tree), and
+ // size the frame to the measured content height so the dialog
+ // shrinks for short lists and scrolls past `maxRowsHeight`.
+ ScrollView {
+ rowsContent
+ .background(
+ GeometryReader { proxy in
+ Color.clear.preference(
+ key: RowsHeightPreferenceKey.self,
+ value: proxy.size.height
+ )
+ }
+ )
+ }
+ .frame(height: min(max(measuredContentHeight, 1), maxRowsHeight))
+ .onPreferenceChange(RowsHeightPreferenceKey.self) { measuredContentHeight = $0 }
+ }
+ }
+ }
+
+ private var rowsContent: some View {
+ VStack(spacing: 0) {
+ ForEach(items.indices, id: \.self) { index in
+ let item = items[index]
+ CheckboxRow(
+ item: item,
+ isChecked: selected.contains(item.key),
+ onToggle: { isChecked in
+ if isChecked {
+ selected.insert(item.key)
+ } else {
+ selected.remove(item.key)
+ }
+ }
+ )
+ if index < items.count - 1 {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ }
+ }
+ }
+ }
+}
+
+private struct RowsHeightPreferenceKey: PreferenceKey {
+ static var defaultValue: CGFloat = 0
+ static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
+ value = max(value, nextValue())
+ }
+}
+
+private struct CheckboxRow: View {
+ let item: KeyValueItem
+ let isChecked: Bool
+ let onToggle: (Bool) -> Void
+
+ var body: some View {
+ Button {
+ onToggle(!isChecked)
+ } label: {
+ HStack(alignment: .center, spacing: 14) {
+ Image(systemName: isChecked ? "checkmark.square.fill" : "square")
+ .font(.system(size: 24))
+ .foregroundColor(isChecked ? OS.Color.primary : OS.Color.grey700)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(item.key)
+ .font(OS.Font.bodyLarge)
+ .foregroundColor(OS.Color.bodyText)
+ Text(item.value)
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+ }
+ Spacer(minLength: 0)
+ }
+ .padding(.vertical, 10)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier("remove_checkbox_\(item.key)")
+ }
+}
diff --git a/examples/demo/App/Views/Components/SectionCard.swift b/examples/demo/App/Views/Components/SectionCard.swift
new file mode 100644
index 000000000..bc307771b
--- /dev/null
+++ b/examples/demo/App/Views/Components/SectionCard.swift
@@ -0,0 +1,133 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Section container. Renders a section header (12pt bold uppercase, osGrey700,
+/// letter spacing 0.5) above a vertical stack of children. Per the design spec
+/// children supply their own card chrome — this view only owns the header.
+struct SectionCard: View {
+ let title: String
+ let sectionKey: String
+ let onInfoTap: (() -> Void)?
+ @ViewBuilder let content: () -> Content
+
+ init(
+ title: String,
+ sectionKey: String,
+ onInfoTap: (() -> Void)? = nil,
+ @ViewBuilder content: @escaping () -> Content
+ ) {
+ self.title = title
+ self.sectionKey = sectionKey
+ self.onInfoTap = onInfoTap
+ self.content = content
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: OS.Spacing.cardGap) {
+ HStack(alignment: .center, spacing: 0) {
+ Text(title.uppercased())
+ .font(OS.Font.bodySmall.weight(.bold))
+ .tracking(0.5)
+ .foregroundColor(OS.Color.grey700)
+ Spacer(minLength: 0)
+ if let onInfoTap = onInfoTap {
+ Button(action: onInfoTap) {
+ Image(systemName: "info.circle")
+ .font(.system(size: OS.Layout.infoIconSize))
+ .foregroundColor(OS.Color.grey500)
+ .frame(width: 32, height: 32)
+ }
+ .buttonStyle(.plain)
+ .padding(.trailing, -6)
+ .accessibilityIdentifier("\(sectionKey)_info_icon")
+ }
+ }
+
+ VStack(alignment: .leading, spacing: OS.Spacing.cardGap) {
+ content()
+ }
+ }
+ // SwiftUI does not promote a bare VStack to an accessibility element,
+ // so a plain `.accessibilityIdentifier` here is invisible to XCUITest /
+ // Appium queries (e.g. `scrollToEl('user_section')` would loop until it
+ // hits the scroll cap). `.contain` makes the container queryable while
+ // keeping every child (Texts, Buttons, toggles) individually accessible.
+ .accessibilityElement(children: .contain)
+ .accessibilityIdentifier("\(sectionKey)_section")
+ }
+}
+
+/// Generic value card used at the top of sections (App ID, Push ID, Status, etc.).
+/// Renders rows with a 14pt label and a 12pt value (monospace by default for IDs).
+struct ValueCard: View {
+ struct Row {
+ let label: String
+ let value: String
+ let valueAccessibilityID: String?
+ let monospaced: Bool
+
+ init(label: String, value: String, valueAccessibilityID: String? = nil, monospaced: Bool = false) {
+ self.label = label
+ self.value = value
+ self.valueAccessibilityID = valueAccessibilityID
+ self.monospaced = monospaced
+ }
+ }
+
+ let rows: [Row]
+
+ var body: some View {
+ VStack(spacing: 0) {
+ ForEach(rows.indices, id: \.self) { index in
+ let row = rows[index]
+ HStack(alignment: .center, spacing: 12) {
+ Text(row.label)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ Text(row.value)
+ .font(row.monospaced ? OS.Font.mono12 : OS.Font.bodySmall)
+ .foregroundColor(OS.Color.bodyText)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .accessibilityIdentifier(row.valueAccessibilityID ?? "")
+ }
+ .padding(.vertical, 4)
+
+ if index < rows.count - 1 {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+ .osCard()
+ }
+}
diff --git a/examples/demo/App/Views/Components/ToastView.swift b/examples/demo/App/Views/Components/ToastView.swift
new file mode 100644
index 000000000..9c3845699
--- /dev/null
+++ b/examples/demo/App/Views/Components/ToastView.swift
@@ -0,0 +1,81 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// A toast notification view that appears at the bottom of the screen
+struct ToastView: View {
+ let message: String
+
+ var body: some View {
+ Text(message)
+ .font(.subheadline)
+ .foregroundColor(.white)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+ .background(Color.black.opacity(0.8))
+ .cornerRadius(8)
+ .shadow(radius: 4)
+ .accessibilityIdentifier("snackbar_toast")
+ }
+}
+
+/// A view modifier that overlays a toast message
+struct ToastModifier: ViewModifier {
+ @Binding var message: String?
+
+ func body(content: Content) -> some View {
+ ZStack {
+ content
+
+ if let message = message {
+ VStack {
+ Spacer()
+ ToastView(message: message)
+ .padding(.bottom, 32)
+ .transition(.move(edge: .bottom).combined(with: .opacity))
+ }
+ .animation(.easeInOut(duration: 0.3), value: message)
+ }
+ }
+ }
+}
+
+extension View {
+ /// Adds a toast overlay to the view
+ func toast(message: Binding) -> some View {
+ modifier(ToastModifier(message: message))
+ }
+}
+
+#Preview {
+ VStack {
+ Text("Content")
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .toast(message: .constant("This is a toast message"))
+}
diff --git a/examples/demo/App/Views/Components/ToggleRow.swift b/examples/demo/App/Views/Components/ToggleRow.swift
new file mode 100644
index 000000000..59d1ceeba
--- /dev/null
+++ b/examples/demo/App/Views/Components/ToggleRow.swift
@@ -0,0 +1,74 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Toggle row inside a card. Label (14) + optional supporting subtitle (12 osGrey600)
+/// on the left, native switch on the right.
+struct ToggleRow: View {
+ let label: String
+ let description: String?
+ let isOn: Binding
+ let isDisabled: Bool
+ let accessibilityID: String
+
+ init(
+ label: String,
+ description: String? = nil,
+ isOn: Binding,
+ isDisabled: Bool = false,
+ accessibilityID: String
+ ) {
+ self.label = label
+ self.description = description
+ self.isOn = isOn
+ self.isDisabled = isDisabled
+ self.accessibilityID = accessibilityID
+ }
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 12) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(label)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ if let description = description {
+ Text(description)
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+ }
+ }
+ Spacer(minLength: 0)
+ Toggle("", isOn: isOn)
+ .labelsHidden()
+ .tint(OS.Color.primary)
+ .disabled(isDisabled)
+ .accessibilityIdentifier(accessibilityID)
+ }
+ .osCard()
+ }
+}
diff --git a/examples/demo/App/Views/Components/TooltipDialog.swift b/examples/demo/App/Views/Components/TooltipDialog.swift
new file mode 100644
index 000000000..f9392eb0d
--- /dev/null
+++ b/examples/demo/App/Views/Components/TooltipDialog.swift
@@ -0,0 +1,101 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Tooltip dialog body shown when the user taps a section's info icon.
+/// Single OK action — no Cancel. Presented as a centered modal via
+/// `osCenteredDialog`, so this view renders only the card chrome.
+struct TooltipDialog: View {
+ let tooltip: TooltipData
+ let onClose: () -> Void
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text(tooltip.title)
+ .font(.system(size: 24, weight: .regular))
+ .foregroundColor(OS.Color.bodyText)
+ .accessibilityIdentifier("tooltip_title")
+
+ ViewThatFits(in: .vertical) {
+ bodyContent
+ ScrollView { bodyContent }
+ }
+
+ HStack {
+ Spacer()
+ OSDialogActionButton(
+ title: "OK",
+ accessibilityID: "tooltip_ok_button",
+ isEnabled: true,
+ action: onClose
+ )
+ }
+ .padding(.top, 8)
+ }
+ .padding(24)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(OS.Color.cardBackground)
+ // `.contain` mirrors `SectionCard`: without it, the outer
+ // `.accessibilityIdentifier` would propagate down and overwrite each
+ // child's identifier (`tooltip_title`, `tooltip_description`,
+ // `tooltip_ok_button`), making them unreachable to XCUITest.
+ .accessibilityElement(children: .contain)
+ .accessibilityIdentifier("tooltip_dialog")
+ }
+
+ private var bodyContent: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ Text(tooltip.description)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .fixedSize(horizontal: false, vertical: true)
+ .accessibilityIdentifier("tooltip_description")
+
+ if let options = tooltip.options, !options.isEmpty {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ .padding(.vertical, 4)
+
+ VStack(alignment: .leading, spacing: 12) {
+ ForEach(options, id: \.name) { option in
+ VStack(alignment: .leading, spacing: 4) {
+ Text(option.name)
+ .font(OS.Font.bodyMedium.weight(.semibold))
+ .foregroundColor(OS.Color.bodyText)
+ Text(option.description)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Components/TrackEventDialog.swift b/examples/demo/App/Views/Components/TrackEventDialog.swift
new file mode 100644
index 000000000..37d62161e
--- /dev/null
+++ b/examples/demo/App/Views/Components/TrackEventDialog.swift
@@ -0,0 +1,103 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Centered dialog that captures an event name plus an optional
+/// JSON properties payload.
+struct TrackEventDialog: View {
+ let onTrack: (String, [String: Any]?) -> Void
+ let onCancel: () -> Void
+
+ @State private var name: String = ""
+ @State private var propertiesText: String = ""
+ @State private var error: String?
+
+ var body: some View {
+ OSDialog(
+ title: "Track Event",
+ confirmLabel: "Track",
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: "event_track_button",
+ cancelAccessibilityID: "event_cancel_button",
+ onConfirm: submit,
+ onCancel: onCancel
+ ) {
+ VStack(alignment: .leading, spacing: 12) {
+ OSTextField(
+ placeholder: "Event Name",
+ text: $name,
+ accessibilityID: "event_name_input"
+ )
+
+ Text("Properties (JSON, optional)")
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+
+ OSTextEditor(
+ placeholder: "{ \"key\": \"value\" }",
+ text: $propertiesText,
+ minHeight: 120,
+ accessibilityID: "event_properties_input"
+ )
+
+ if let error = error {
+ Text(error)
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.primary)
+ .accessibilityIdentifier("event_properties_error")
+ }
+ }
+ }
+ }
+
+ private var isValid: Bool {
+ !name.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+
+ private func submit() {
+ let trimmedName = name.trimmingCharacters(in: .whitespaces)
+ let trimmedProps = propertiesText.trimmingCharacters(in: .whitespaces)
+
+ if trimmedProps.isEmpty {
+ onTrack(trimmedName, nil)
+ return
+ }
+
+ guard let data = trimmedProps.data(using: .utf8),
+ let json = try? JSONSerialization.jsonObject(with: data) else {
+ error = "Properties must be valid JSON"
+ return
+ }
+ guard let dict = json as? [String: Any] else {
+ error = "Properties must be a JSON object"
+ return
+ }
+ error = nil
+ onTrack(trimmedName, dict)
+ }
+}
diff --git a/examples/demo/App/Views/ContentView.swift b/examples/demo/App/Views/ContentView.swift
new file mode 100644
index 000000000..cdc99be60
--- /dev/null
+++ b/examples/demo/App/Views/ContentView.swift
@@ -0,0 +1,122 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Root view composing every section in the same order as the Capacitor demo.
+struct ContentView: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @EnvironmentObject var toast: ToastPresenter
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(alignment: .leading, spacing: OS.Spacing.sectionGap) {
+ AppSection()
+ UserSection()
+ PushSection()
+ SendPushSection()
+ InAppSection()
+ SendIamSection()
+ AliasesSection()
+ EmailsSection()
+ SmsSection()
+ TagsSection()
+ OutcomesSection()
+ TriggersSection()
+ CustomEventsSection()
+ LocationSection()
+ LiveActivitySection()
+ }
+ .padding(.horizontal, OS.Spacing.pagePadding)
+ .padding(.top, OS.Spacing.pagePadding)
+ .padding(.bottom, OS.Spacing.sectionGap)
+ }
+ // `main_scroll_view` is anchored to the SwiftUI `ScrollView` (not
+ // the inner `VStack`) so XCUITest exposes it as
+ // `XCUIElementTypeScrollView` with the visible viewport's rect.
+ // Anchoring on the inner `VStack` reported the full content rect
+ // (multiple screens tall), causing WDIO `swipe` to compute
+ // gesture coordinates outside the viewport — iOS clipped those
+ // to the visible region and the swipe registered as a tap on
+ // whatever button sat there (e.g. `send_sound_button`). The
+ // ScrollView identifier is read by `waitForAppReady` and by
+ // Android's `scrollIntoView` `scrollableElement` param.
+ .accessibilityIdentifier("main_scroll_view")
+ .background(OS.Color.lightBackground.ignoresSafeArea())
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbarBackground(OS.Color.primary, for: .navigationBar)
+ .toolbarBackground(.visible, for: .navigationBar)
+ .toolbarColorScheme(.dark, for: .navigationBar)
+ .toolbar { toolbarContent }
+ }
+ .osCenteredDialog(
+ isPresented: Binding(
+ get: { viewModel.activeTooltip != nil },
+ set: { isPresented in if !isPresented { viewModel.dismissTooltip() } }
+ )
+ ) {
+ if let tooltip = viewModel.activeTooltip {
+ TooltipDialog(tooltip: tooltip, onClose: { viewModel.dismissTooltip() })
+ }
+ }
+ .toast(message: $toast.message)
+ // Auto-prompt for notification permission on first appear, matching the
+ // Capacitor / Flutter / React Native demos (which all prompt from their
+ // home screen's mount lifecycle). This races the OneSignal iOS-params
+ // response: the standard alert shows before the SDK can register for
+ // provisional authorization (which would otherwise silently grant
+ // permission and skip the prompt entirely).
+ .task {
+ viewModel.promptPushPermission()
+ }
+ }
+
+ @ToolbarContentBuilder
+ private var toolbarContent: some ToolbarContent {
+ ToolbarItem(placement: .principal) {
+ HStack(spacing: 6) {
+ Image("onesignal_logo")
+ .renderingMode(.template)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(height: 22)
+ .foregroundColor(.white)
+ Text("iOS")
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(.white)
+ }
+ .accessibilityIdentifier("brand_title")
+ }
+ }
+}
+
+#Preview {
+ ContentView()
+ .environmentObject(OneSignalViewModel())
+ .environmentObject(ToastPresenter())
+}
diff --git a/examples/demo/App/Views/Sections/AliasesSection.swift b/examples/demo/App/Views/Sections/AliasesSection.swift
new file mode 100644
index 000000000..c6772a5be
--- /dev/null
+++ b/examples/demo/App/Views/Sections/AliasesSection.swift
@@ -0,0 +1,75 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+struct AliasesSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+ @State private var addMultipleOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "ALIASES",
+ sectionKey: "aliases",
+ onInfoTap: { viewModel.showTooltip(for: "aliases") }
+ ) {
+ PairList(
+ items: viewModel.aliases,
+ emptyText: "No aliases added",
+ sectionKey: "aliases"
+ )
+
+ ActionButton("ADD ALIAS", accessibilityID: "add_alias_button") {
+ addOpen = true
+ }
+ ActionButton("ADD MULTIPLE ALIASES", accessibilityID: "add_multiple_aliases_button") {
+ addMultipleOpen = true
+ }
+ }
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .alias,
+ onAdd: { key, value in
+ viewModel.addAlias(label: key, id: value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $addMultipleOpen) {
+ MultiPairInputDialog(
+ type: .aliases,
+ onAdd: { pairs in
+ viewModel.addAliases(pairs)
+ addMultipleOpen = false
+ },
+ onCancel: { addMultipleOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/AppSection.swift b/examples/demo/App/Views/Sections/AppSection.swift
new file mode 100644
index 000000000..2b772ae91
--- /dev/null
+++ b/examples/demo/App/Views/Sections/AppSection.swift
@@ -0,0 +1,68 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// App ID display + consent toggles, mirroring the Capacitor AppSection
+struct AppSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+
+ var body: some View {
+ SectionCard(title: "APP", sectionKey: "app") {
+ ValueCard(rows: [
+ ValueCard.Row(
+ label: "App ID",
+ value: viewModel.appId,
+ valueAccessibilityID: "app_id_value",
+ monospaced: true
+ )
+ ])
+
+ ToggleRow(
+ label: "Consent Required",
+ description: "Require consent before SDK processes data",
+ isOn: Binding(
+ get: { viewModel.consentRequired },
+ set: { viewModel.setConsentRequired($0) }
+ ),
+ accessibilityID: "consent_required_toggle"
+ )
+
+ if viewModel.consentRequired {
+ ToggleRow(
+ label: "Privacy Consent",
+ description: "Consent given for data collection",
+ isOn: Binding(
+ get: { viewModel.consentGiven },
+ set: { viewModel.setConsentGiven($0) }
+ ),
+ accessibilityID: "privacy_consent_toggle"
+ )
+ }
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/CustomEventsSection.swift b/examples/demo/App/Views/Sections/CustomEventsSection.swift
new file mode 100644
index 000000000..01d2e742e
--- /dev/null
+++ b/examples/demo/App/Views/Sections/CustomEventsSection.swift
@@ -0,0 +1,56 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+struct CustomEventsSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @EnvironmentObject var toast: ToastPresenter
+ @State private var open = false
+
+ var body: some View {
+ SectionCard(
+ title: "CUSTOM EVENTS",
+ sectionKey: "custom_events",
+ onInfoTap: { viewModel.showTooltip(for: "customEvents") }
+ ) {
+ ActionButton("TRACK EVENT", accessibilityID: "track_event_button") {
+ open = true
+ }
+ }
+ .osCenteredDialog(isPresented: $open) {
+ TrackEventDialog(
+ onTrack: { name, properties in
+ viewModel.trackEvent(name: name, properties: properties)
+ toast.show("Event tracked: \(name)")
+ open = false
+ },
+ onCancel: { open = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/EmailsSection.swift b/examples/demo/App/Views/Sections/EmailsSection.swift
new file mode 100644
index 000000000..ba9ef1cad
--- /dev/null
+++ b/examples/demo/App/Views/Sections/EmailsSection.swift
@@ -0,0 +1,62 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+struct EmailsSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "EMAILS",
+ sectionKey: "emails",
+ onInfoTap: { viewModel.showTooltip(for: "emails") }
+ ) {
+ SingleList(
+ items: viewModel.emails,
+ emptyText: "No emails added",
+ sectionKey: "emails",
+ onRemove: { viewModel.removeEmail($0) }
+ )
+
+ ActionButton("ADD EMAIL", accessibilityID: "add_email_button") {
+ addOpen = true
+ }
+ }
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .email,
+ onAdd: { _, value in
+ viewModel.addEmail(value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/InAppSection.swift b/examples/demo/App/Views/Sections/InAppSection.swift
new file mode 100644
index 000000000..84aa00ee4
--- /dev/null
+++ b/examples/demo/App/Views/Sections/InAppSection.swift
@@ -0,0 +1,51 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// In-app messaging pause toggle
+struct InAppSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+
+ var body: some View {
+ SectionCard(
+ title: "IN-APP MESSAGING",
+ sectionKey: "iam",
+ onInfoTap: { viewModel.showTooltip(for: "inAppMessaging") }
+ ) {
+ ToggleRow(
+ label: "Pause In-App Messages",
+ description: "Toggle in-app message display",
+ isOn: Binding(
+ get: { viewModel.isInAppMessagesPaused },
+ set: { viewModel.setIamPaused($0) }
+ ),
+ accessibilityID: "pause_iam_toggle"
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/LiveActivitySection.swift b/examples/demo/App/Views/Sections/LiveActivitySection.swift
new file mode 100644
index 000000000..136c6564d
--- /dev/null
+++ b/examples/demo/App/Views/Sections/LiveActivitySection.swift
@@ -0,0 +1,144 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Live Activities (iOS 16.1+) section with activity ID + order # inputs and status cycler.
+/// Mirrors the Capacitor demo's LiveActivitySection.
+struct LiveActivitySection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+
+ @State private var activityId: String = "order-1"
+ @State private var orderNumber: String = "ORD-1234"
+ @State private var statusIndex: Int = 0
+
+ private let statuses: [LiveActivityStatus] = [.preparing, .onTheWay, .delivered]
+
+ var body: some View {
+ SectionCard(
+ title: "LIVE ACTIVITIES",
+ sectionKey: "live_activities",
+ onInfoTap: { viewModel.showTooltip(for: "liveActivities") }
+ ) {
+ inputCard
+
+ ActionButton(
+ "START LIVE ACTIVITY",
+ isDisabled: trimmedActivityId.isEmpty,
+ accessibilityID: "start_live_activity_button"
+ ) {
+ statusIndex = 0
+ viewModel.startLiveActivity(
+ activityId: trimmedActivityId,
+ orderNumber: orderNumber.trimmingCharacters(in: .whitespacesAndNewlines),
+ status: statuses[0]
+ )
+ }
+
+ ActionButton(
+ updateButtonTitle,
+ isDisabled: trimmedActivityId.isEmpty || !LiveActivityController.hasApiKey,
+ accessibilityID: "update_live_activity_button"
+ ) {
+ let nextIndex = (statusIndex + 1) % statuses.count
+ viewModel.updateLiveActivity(
+ activityId: trimmedActivityId,
+ status: statuses[nextIndex]
+ )
+ statusIndex = nextIndex
+ }
+
+ ActionButton(
+ "END LIVE ACTIVITY",
+ style: .outline,
+ isDisabled: trimmedActivityId.isEmpty || !LiveActivityController.hasApiKey,
+ accessibilityID: "end_live_activity_button"
+ ) {
+ viewModel.endLiveActivity(activityId: trimmedActivityId)
+ }
+
+ if !LiveActivityController.hasApiKey {
+ Text("Set ONESIGNAL_API_KEY in Secrets.plist to enable update & end")
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .accessibilityIdentifier("live_activities_hint")
+ }
+ }
+ }
+
+ private var trimmedActivityId: String {
+ activityId.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+
+ private var nextStatus: LiveActivityStatus {
+ statuses[(statusIndex + 1) % statuses.count]
+ }
+
+ private var updateButtonTitle: String {
+ "UPDATE → \(nextStatus.displayName.uppercased())"
+ }
+
+ private var inputCard: some View {
+ VStack(spacing: 4) {
+ inlineRow(
+ label: "Activity ID",
+ placeholder: "Activity ID",
+ text: $activityId,
+ accessibilityID: "live_activity_id_input"
+ )
+ inlineRow(
+ label: "Order #",
+ placeholder: "Order #",
+ text: $orderNumber,
+ accessibilityID: "live_activity_order_number"
+ )
+ }
+ .osCard()
+ }
+
+ private func inlineRow(
+ label: String,
+ placeholder: String,
+ text: Binding,
+ accessibilityID: String
+ ) -> some View {
+ HStack(alignment: .center, spacing: 8) {
+ Text(label)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .frame(minWidth: OS.Layout.inlineLabelMinWidth, alignment: .leading)
+ TextField(placeholder, text: text)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .multilineTextAlignment(.trailing)
+ .autocorrectionDisabled()
+ .textInputAutocapitalization(.never)
+ .accessibilityIdentifier(accessibilityID)
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/LocationSection.swift b/examples/demo/App/Views/Sections/LocationSection.swift
new file mode 100644
index 000000000..41079cea7
--- /dev/null
+++ b/examples/demo/App/Views/Sections/LocationSection.swift
@@ -0,0 +1,60 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+struct LocationSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @EnvironmentObject var toast: ToastPresenter
+
+ var body: some View {
+ SectionCard(
+ title: "LOCATION",
+ sectionKey: "location",
+ onInfoTap: { viewModel.showTooltip(for: "location") }
+ ) {
+ ToggleRow(
+ label: "Location Shared",
+ description: "Share device location with OneSignal",
+ isOn: Binding(
+ get: { viewModel.isLocationShared },
+ set: { viewModel.setLocationShared($0) }
+ ),
+ accessibilityID: "location_shared_toggle"
+ )
+
+ ActionButton("PROMPT LOCATION", accessibilityID: "prompt_location_button") {
+ viewModel.promptLocation()
+ }
+
+ ActionButton("CHECK LOCATION", accessibilityID: "check_location_button") {
+ let shared = viewModel.checkLocationShared()
+ toast.show("Location shared: \(shared)")
+ }
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/OutcomesSection.swift b/examples/demo/App/Views/Sections/OutcomesSection.swift
new file mode 100644
index 000000000..f4b84f259
--- /dev/null
+++ b/examples/demo/App/Views/Sections/OutcomesSection.swift
@@ -0,0 +1,67 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+struct OutcomesSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @EnvironmentObject var toast: ToastPresenter
+ @State private var open = false
+
+ var body: some View {
+ SectionCard(
+ title: "OUTCOME EVENTS",
+ sectionKey: "outcomes",
+ onInfoTap: { viewModel.showTooltip(for: "outcomes") }
+ ) {
+ ActionButton("SEND OUTCOME", accessibilityID: "send_outcome_button") {
+ open = true
+ }
+ }
+ .osCenteredDialog(isPresented: $open) {
+ OutcomeDialog(
+ onSend: { name, mode, value in
+ switch mode {
+ case .normal:
+ viewModel.sendOutcome(name)
+ toast.show("Outcome sent: \(name)")
+ case .unique:
+ viewModel.sendUniqueOutcome(name)
+ toast.show("Unique outcome sent: \(name)")
+ case .value:
+ if let value = value {
+ viewModel.sendOutcome(name, value: value)
+ toast.show("Outcome sent: \(name) = \(value)")
+ }
+ }
+ open = false
+ },
+ onCancel: { open = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/PushSection.swift b/examples/demo/App/Views/Sections/PushSection.swift
new file mode 100644
index 000000000..d78dfba54
--- /dev/null
+++ b/examples/demo/App/Views/Sections/PushSection.swift
@@ -0,0 +1,97 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Push subscription ID, opt-in toggle, and prompt-for-permission CTA
+struct PushSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+
+ var body: some View {
+ SectionCard(
+ title: "PUSH",
+ sectionKey: "push",
+ onInfoTap: { viewModel.showTooltip(for: "push") }
+ ) {
+ VStack(spacing: 0) {
+ pushIdRow
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ .padding(.vertical, 4)
+ pushEnabledRow
+ }
+ .osCard()
+
+ if !viewModel.hasNotificationPermission {
+ ActionButton(
+ "PROMPT PUSH",
+ accessibilityID: "prompt_push_button"
+ ) {
+ viewModel.promptPushPermission()
+ }
+ }
+ }
+ }
+
+ private var pushIdRow: some View {
+ HStack(alignment: .center, spacing: 12) {
+ Text("Push ID")
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ Text(viewModel.pushSubscriptionId ?? "—")
+ .font(OS.Font.mono12)
+ .foregroundColor(OS.Color.bodyText)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .accessibilityIdentifier("push_id_value")
+ }
+ .padding(.vertical, 4)
+ }
+
+ private var pushEnabledRow: some View {
+ HStack(alignment: .center, spacing: 12) {
+ Text("Push Enabled")
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ Toggle(
+ "",
+ isOn: Binding(
+ get: { viewModel.isPushEnabled },
+ set: { viewModel.setPushEnabled($0) }
+ )
+ )
+ .labelsHidden()
+ .tint(OS.Color.primary)
+ .disabled(!viewModel.hasNotificationPermission)
+ .accessibilityIdentifier("push_enabled_toggle")
+ }
+ .padding(.vertical, 4)
+ }
+}
diff --git a/examples/demo/App/Views/Sections/SendIamSection.swift b/examples/demo/App/Views/Sections/SendIamSection.swift
new file mode 100644
index 000000000..9fd851a6e
--- /dev/null
+++ b/examples/demo/App/Views/Sections/SendIamSection.swift
@@ -0,0 +1,50 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Buttons that add an `iam_type` trigger so dashboard IAM rules can fire
+struct SendIamSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+
+ var body: some View {
+ SectionCard(
+ title: "SEND IN-APP MESSAGE",
+ sectionKey: "send_iam",
+ onInfoTap: { viewModel.showTooltip(for: "sendInAppMessage") }
+ ) {
+ ForEach(InAppMessageType.allCases) { type in
+ ActionButton(
+ type.rawValue.uppercased(),
+ accessibilityID: "send_iam_\(type.triggerValue)_button"
+ ) {
+ viewModel.sendIamTrigger(type)
+ }
+ }
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/SendPushSection.swift b/examples/demo/App/Views/Sections/SendPushSection.swift
new file mode 100644
index 000000000..329fa2c8e
--- /dev/null
+++ b/examples/demo/App/Views/Sections/SendPushSection.swift
@@ -0,0 +1,71 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Buttons that fire test pushes via the OneSignal REST API
+struct SendPushSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var customOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "SEND PUSH NOTIFICATION",
+ sectionKey: "send_push",
+ onInfoTap: { viewModel.showTooltip(for: "sendPushNotification") }
+ ) {
+ ActionButton("SIMPLE", accessibilityID: "send_simple_button") {
+ viewModel.sendNotification(.simple)
+ }
+ ActionButton("WITH IMAGE", accessibilityID: "send_image_button") {
+ viewModel.sendNotification(.withImage)
+ }
+ ActionButton("WITH SOUND", accessibilityID: "send_sound_button") {
+ viewModel.sendNotification(.withSound)
+ }
+ ActionButton("CUSTOM", accessibilityID: "send_custom_button") {
+ customOpen = true
+ }
+ ActionButton(
+ "CLEAR ALL",
+ style: .outline,
+ accessibilityID: "clear_all_button"
+ ) {
+ viewModel.clearAllNotifications()
+ }
+ }
+ .osCenteredDialog(isPresented: $customOpen) {
+ CustomNotificationDialog(
+ onSend: { title, body in
+ viewModel.sendCustomNotification(title: title, body: body)
+ customOpen = false
+ },
+ onCancel: { customOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/SmsSection.swift b/examples/demo/App/Views/Sections/SmsSection.swift
new file mode 100644
index 000000000..d8deb5a1a
--- /dev/null
+++ b/examples/demo/App/Views/Sections/SmsSection.swift
@@ -0,0 +1,62 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+struct SmsSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "SMS",
+ sectionKey: "sms",
+ onInfoTap: { viewModel.showTooltip(for: "sms") }
+ ) {
+ SingleList(
+ items: viewModel.smsNumbers,
+ emptyText: "No SMS added",
+ sectionKey: "sms",
+ onRemove: { viewModel.removeSms($0) }
+ )
+
+ ActionButton("ADD SMS", accessibilityID: "add_sms_button") {
+ addOpen = true
+ }
+ }
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .sms,
+ onAdd: { _, value in
+ viewModel.addSms(value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/TagsSection.swift b/examples/demo/App/Views/Sections/TagsSection.swift
new file mode 100644
index 000000000..a7e775655
--- /dev/null
+++ b/examples/demo/App/Views/Sections/TagsSection.swift
@@ -0,0 +1,101 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+struct TagsSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+ @State private var addMultipleOpen = false
+ @State private var removeOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "TAGS",
+ sectionKey: "tags",
+ onInfoTap: { viewModel.showTooltip(for: "tags") }
+ ) {
+ PairList(
+ items: viewModel.tags,
+ emptyText: "No tags added",
+ sectionKey: "tags",
+ onRemove: { key in
+ if let item = viewModel.tags.first(where: { $0.key == key }) {
+ viewModel.removeTag(item)
+ }
+ }
+ )
+
+ ActionButton("ADD TAG", accessibilityID: "add_tag_button") {
+ addOpen = true
+ }
+ ActionButton("ADD MULTIPLE TAGS", accessibilityID: "add_multiple_tags_button") {
+ addMultipleOpen = true
+ }
+ if !viewModel.tags.isEmpty {
+ ActionButton(
+ "REMOVE TAGS",
+ style: .outline,
+ accessibilityID: "remove_tags_button"
+ ) {
+ removeOpen = true
+ }
+ }
+ }
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .tag,
+ onAdd: { key, value in
+ viewModel.addTag(key: key, value: value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $addMultipleOpen) {
+ MultiPairInputDialog(
+ type: .tags,
+ onAdd: { pairs in
+ viewModel.addTags(pairs)
+ addMultipleOpen = false
+ },
+ onCancel: { addMultipleOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $removeOpen) {
+ RemoveMultiDialog(
+ type: .tags,
+ items: viewModel.tags,
+ onRemove: { keys in
+ viewModel.removeSelectedTags(keys)
+ removeOpen = false
+ },
+ onCancel: { removeOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/TriggersSection.swift b/examples/demo/App/Views/Sections/TriggersSection.swift
new file mode 100644
index 000000000..0838045af
--- /dev/null
+++ b/examples/demo/App/Views/Sections/TriggersSection.swift
@@ -0,0 +1,108 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+struct TriggersSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+ @State private var addMultipleOpen = false
+ @State private var removeOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "TRIGGERS",
+ sectionKey: "triggers",
+ onInfoTap: { viewModel.showTooltip(for: "triggers") }
+ ) {
+ PairList(
+ items: viewModel.triggers,
+ emptyText: "No triggers added",
+ sectionKey: "triggers",
+ onRemove: { key in
+ if let item = viewModel.triggers.first(where: { $0.key == key }) {
+ viewModel.removeTrigger(item)
+ }
+ }
+ )
+
+ ActionButton("ADD TRIGGER", accessibilityID: "add_trigger_button") {
+ addOpen = true
+ }
+ ActionButton("ADD MULTIPLE TRIGGERS", accessibilityID: "add_multiple_triggers_button") {
+ addMultipleOpen = true
+ }
+ if !viewModel.triggers.isEmpty {
+ ActionButton(
+ "REMOVE TRIGGERS",
+ style: .outline,
+ accessibilityID: "remove_triggers_button"
+ ) {
+ removeOpen = true
+ }
+ ActionButton(
+ "CLEAR ALL TRIGGERS",
+ style: .outline,
+ accessibilityID: "clear_triggers_button"
+ ) {
+ viewModel.clearTriggers()
+ }
+ }
+ }
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .trigger,
+ onAdd: { key, value in
+ viewModel.addTrigger(key: key, value: value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $addMultipleOpen) {
+ MultiPairInputDialog(
+ type: .triggers,
+ onAdd: { pairs in
+ viewModel.addTriggers(pairs)
+ addMultipleOpen = false
+ },
+ onCancel: { addMultipleOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $removeOpen) {
+ RemoveMultiDialog(
+ type: .triggers,
+ items: viewModel.triggers,
+ onRemove: { keys in
+ viewModel.removeSelectedTriggers(keys)
+ removeOpen = false
+ },
+ onCancel: { removeOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/UserSection.swift b/examples/demo/App/Views/Sections/UserSection.swift
new file mode 100644
index 000000000..0e0bb5067
--- /dev/null
+++ b/examples/demo/App/Views/Sections/UserSection.swift
@@ -0,0 +1,79 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Login/logout + status display, mirroring the Capacitor UserSection
+struct UserSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var loginOpen = false
+
+ var body: some View {
+ SectionCard(title: "USER", sectionKey: "user") {
+ ValueCard(rows: [
+ ValueCard.Row(
+ label: "Status",
+ value: viewModel.isLoggedIn ? "Logged In" : "Anonymous",
+ valueAccessibilityID: "user_status_value"
+ ),
+ ValueCard.Row(
+ label: "External ID",
+ value: viewModel.externalUserId ?? "—",
+ valueAccessibilityID: "user_external_id_value",
+ monospaced: true
+ )
+ ])
+
+ ActionButton(
+ viewModel.loginButtonTitle,
+ accessibilityID: "login_user_button"
+ ) {
+ loginOpen = true
+ }
+
+ if viewModel.isLoggedIn {
+ ActionButton(
+ "LOGOUT USER",
+ style: .outline,
+ accessibilityID: "logout_user_button"
+ ) {
+ viewModel.logout()
+ }
+ }
+ }
+ .osCenteredDialog(isPresented: $loginOpen) {
+ AddItemDialog(
+ itemType: .externalUserId,
+ onAdd: { _, value in
+ viewModel.login(externalId: value)
+ loginOpen = false
+ },
+ onCancel: { loginOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Theme.swift b/examples/demo/App/Views/Theme.swift
new file mode 100644
index 000000000..645f5bf0b
--- /dev/null
+++ b/examples/demo/App/Views/Theme.swift
@@ -0,0 +1,119 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import SwiftUI
+
+/// Design tokens shared across the demo. Mirrors the CSS variables and tables in
+/// `sdk-shared/demo/styles.md`.
+enum OS {
+
+ // MARK: Colors
+
+ enum Color {
+ static let primary = SwiftUI.Color(red: 0xE5/255, green: 0x4B/255, blue: 0x4D/255)
+ static let primaryPressed = SwiftUI.Color(red: 0xC3/255, green: 0x3F/255, blue: 0x41/255)
+ static let success = SwiftUI.Color(red: 0x34/255, green: 0xA8/255, blue: 0x53/255)
+ static let grey700 = SwiftUI.Color(red: 0x61/255, green: 0x61/255, blue: 0x61/255)
+ static let grey600 = SwiftUI.Color(red: 0x75/255, green: 0x75/255, blue: 0x75/255)
+ static let grey500 = SwiftUI.Color(red: 0x9E/255, green: 0x9E/255, blue: 0x9E/255)
+ static let lightBackground = SwiftUI.Color(red: 0xF8/255, green: 0xF9/255, blue: 0xFA/255)
+ static let cardBackground = SwiftUI.Color.white
+ static let cardBorder = SwiftUI.Color.black.opacity(0.1)
+ static let divider = SwiftUI.Color(red: 0xE8/255, green: 0xEA/255, blue: 0xED/255)
+ static let warningBackground = SwiftUI.Color(red: 0xFF/255, green: 0xF8/255, blue: 0xE1/255)
+ static let backdrop = SwiftUI.Color.black.opacity(0.54)
+ static let bodyText = SwiftUI.Color(red: 0x21/255, green: 0x21/255, blue: 0x21/255)
+ }
+
+ // MARK: Spacing
+
+ enum Spacing {
+ static let cardGap: CGFloat = 8
+ static let sectionGap: CGFloat = 24
+ static let pagePadding: CGFloat = 16
+ static let cardPadding: CGFloat = 12
+ }
+
+ // MARK: Radii
+
+ enum Radius {
+ static let card: CGFloat = 12
+ static let button: CGFloat = 8
+ static let input: CGFloat = 8
+ static let modal: CGFloat = 28
+ }
+
+ // MARK: Typography
+
+ enum Font {
+ static let bodyLarge = SwiftUI.Font.system(size: 16, weight: .regular)
+ static let bodyMedium = SwiftUI.Font.system(size: 14, weight: .regular)
+ static let bodySmall = SwiftUI.Font.system(size: 12, weight: .regular)
+ static let mono12 = SwiftUI.Font.system(size: 12, weight: .regular, design: .monospaced)
+ static let mono14 = SwiftUI.Font.system(size: 14, weight: .regular, design: .monospaced)
+ }
+
+ // MARK: Layout constants
+
+ enum Layout {
+ static let buttonHeight: CGFloat = 48
+ static let cardBorderWidth: CGFloat = 2
+ static let inputBorderWidth: CGFloat = 1
+ static let dividerHeight: CGFloat = 1
+ static let infoIconSize: CGFloat = 18
+ static let inlineLabelMinWidth: CGFloat = 80
+ static let listMaxVisible: Int = 5
+ }
+}
+
+// MARK: - Card chrome modifier
+
+/// Applies the standard demo card visual: white background, 12 corner radius,
+/// 2px border, no shadow, 12 px inner padding.
+struct CardChrome: ViewModifier {
+ var padding: CGFloat = OS.Spacing.cardPadding
+ var background: Color = OS.Color.cardBackground
+
+ func body(content: Content) -> some View {
+ content
+ .padding(padding)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(background)
+ .clipShape(RoundedRectangle(cornerRadius: OS.Radius.card))
+ .overlay(
+ RoundedRectangle(cornerRadius: OS.Radius.card)
+ .strokeBorder(OS.Color.cardBorder, lineWidth: OS.Layout.cardBorderWidth)
+ )
+ }
+}
+
+extension View {
+ /// Wraps the receiver in the demo's standard card chrome.
+ func osCard(padding: CGFloat = OS.Spacing.cardPadding, background: Color = OS.Color.cardBackground) -> some View {
+ modifier(CardChrome(padding: padding, background: background))
+ }
+}
diff --git a/examples/demo/App/vine_boom.wav b/examples/demo/App/vine_boom.wav
new file mode 100644
index 000000000..626bd5cc5
Binary files /dev/null and b/examples/demo/App/vine_boom.wav differ
diff --git a/examples/demo/Build.xcconfig b/examples/demo/Build.xcconfig
new file mode 100644
index 000000000..8db97b911
--- /dev/null
+++ b/examples/demo/Build.xcconfig
@@ -0,0 +1,8 @@
+// Demo build settings auto-included by every target via project.yml.
+//
+// Per-developer values (Apple DEVELOPMENT_TEAM, custom bundle ids, etc.)
+// belong in a sibling `Local.xcconfig` so they never get committed. The
+// optional include below is a no-op when the file is missing, so a fresh
+// clone builds without further setup. See `Local.xcconfig.example`.
+
+#include? "Local.xcconfig"
diff --git a/examples/demo/ExportOptions.plist b/examples/demo/ExportOptions.plist
new file mode 100644
index 000000000..b7361222e
--- /dev/null
+++ b/examples/demo/ExportOptions.plist
@@ -0,0 +1,23 @@
+
+
+
+
+ method
+ development
+ teamID
+ 99SW8E36CT
+ signingStyle
+ manual
+ stripSwiftSymbols
+
+ provisioningProfiles
+
+ com.onesignal.example
+ Appium Demo - Main
+ com.onesignal.example.NSE
+ Appium Demo - NSE
+ com.onesignal.example.LA
+ Appium Demo - Live Activity
+
+
+
diff --git a/examples/demo/Local.xcconfig.example b/examples/demo/Local.xcconfig.example
new file mode 100644
index 000000000..8a403f08a
--- /dev/null
+++ b/examples/demo/Local.xcconfig.example
@@ -0,0 +1,15 @@
+// Copy this file to `Local.xcconfig` (gitignored) and uncomment any
+// per-developer overrides you need. Settings here apply to the App,
+// OneSignalNotificationServiceExtension, and OneSignalWidget targets.
+//
+// Anything you set here survives `xcodegen generate`; settings you change
+// only in Xcode's Signing & Capabilities UI are wiped on regeneration.
+
+// Apple Developer team id used by automatic signing. Required to run on a
+// physical device.
+// DEVELOPMENT_TEAM = ABCDE12345
+
+// Switch from automatic to manual signing if you have a specific
+// provisioning profile to attach. Leave on Automatic for most cases.
+// CODE_SIGN_STYLE = Manual
+// PROVISIONING_PROFILE_SPECIFIER = MyProfile
diff --git a/examples/demo/OneSignalNotificationServiceExtension/Info.plist b/examples/demo/OneSignalNotificationServiceExtension/Info.plist
new file mode 100644
index 000000000..ca3cd8f43
--- /dev/null
+++ b/examples/demo/OneSignalNotificationServiceExtension/Info.plist
@@ -0,0 +1,31 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ OneSignalNotificationServiceExtension
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.usernotifications.service
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).NotificationService
+
+
+
diff --git a/examples/demo/OneSignalNotificationServiceExtension/NotificationService.swift b/examples/demo/OneSignalNotificationServiceExtension/NotificationService.swift
new file mode 100644
index 000000000..df9ad5d3c
--- /dev/null
+++ b/examples/demo/OneSignalNotificationServiceExtension/NotificationService.swift
@@ -0,0 +1,71 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import UserNotifications
+import OneSignalExtension
+
+/// Notification Service Extension that hands incoming pushes to OneSignal so it can
+/// download attachments, decrypt confidential pushes, and apply mutable content updates.
+/// Only runs when `mutable_content` is set on the push (which OneSignal sets automatically
+/// for any notification with attachments or action buttons).
+class NotificationService: UNNotificationServiceExtension {
+
+ var contentHandler: ((UNNotificationContent) -> Void)?
+ var receivedRequest: UNNotificationRequest!
+ var bestAttemptContent: UNMutableNotificationContent?
+
+ override func didReceive(
+ _ request: UNNotificationRequest,
+ withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
+ ) {
+ self.receivedRequest = request
+ self.contentHandler = contentHandler
+ self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
+
+ if let bestAttemptContent = bestAttemptContent {
+ // Uncomment to verify the extension is firing during local debug:
+ // print("Running NotificationServiceExtension")
+ // bestAttemptContent.body = "[Modified] " + bestAttemptContent.body
+
+ OneSignalExtension.didReceiveNotificationExtensionRequest(
+ self.receivedRequest,
+ with: bestAttemptContent,
+ withContentHandler: self.contentHandler
+ )
+ }
+ }
+
+ override func serviceExtensionTimeWillExpire() {
+ if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
+ OneSignalExtension.serviceExtensionTimeWillExpireRequest(
+ self.receivedRequest,
+ with: self.bestAttemptContent
+ )
+ contentHandler(bestAttemptContent)
+ }
+ }
+}
diff --git a/examples/demo/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements b/examples/demo/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements
new file mode 100644
index 000000000..c70461e82
--- /dev/null
+++ b/examples/demo/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.onesignal.example.onesignal
+
+
+
diff --git a/examples/demo/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/demo/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000..0afb3cf0e
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/examples/demo/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/demo/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..b121e3bce
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images": [
+ {
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/examples/demo/OneSignalWidget/Assets.xcassets/Contents.json b/examples/demo/OneSignalWidget/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..74d6a722c
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/examples/demo/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/examples/demo/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 000000000..0afb3cf0e
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/examples/demo/OneSignalWidget/Info.plist b/examples/demo/OneSignalWidget/Info.plist
new file mode 100644
index 000000000..a75840841
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Info.plist
@@ -0,0 +1,29 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ OneSignalWidget
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+
+
diff --git a/examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift b/examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift
new file mode 100644
index 000000000..063a36e0c
--- /dev/null
+++ b/examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift
@@ -0,0 +1,38 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import WidgetKit
+import SwiftUI
+
+@main
+struct OneSignalWidgetBundle: WidgetBundle {
+ var body: some Widget {
+ if #available(iOS 16.2, *) {
+ OneSignalWidgetLiveActivity()
+ }
+ }
+}
diff --git a/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift b/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift
new file mode 100644
index 000000000..cf4a029d3
--- /dev/null
+++ b/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift
@@ -0,0 +1,175 @@
+/**
+ * Modified MIT License
+ *
+ * Copyright 2024 OneSignal
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * 1. The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * 2. All copies of substantial portions of the Software may only be used in connection
+ * with services provided by OneSignal.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+import ActivityKit
+import WidgetKit
+import SwiftUI
+import OneSignalLiveActivities
+
+/// Live Activity widget that renders the order tracking flow used by the demo.
+/// Uses `DefaultLiveActivityAttributes` (provided by the OneSignal SDK) so the same
+/// data shape works between `OneSignal.LiveActivities.startDefault(...)` and remote
+/// `event_updates` payloads sent via the REST API.
+@available(iOS 16.2, *)
+struct OneSignalWidgetLiveActivity: Widget {
+
+ var body: some WidgetConfiguration {
+ ActivityConfiguration(for: DefaultLiveActivityAttributes.self) { context in
+ let orderNumber = context.attributes.data["orderNumber"]?.asString() ?? "Order"
+ let status = context.state.data["status"]?.asString() ?? "preparing"
+ let message = context.state.data["message"]?.asString() ?? "Your order is being prepared"
+ let eta = context.state.data["estimatedTime"]?.asString() ?? ""
+
+ VStack(spacing: 10) {
+ HStack {
+ Text(orderNumber)
+ .font(.caption)
+ .foregroundColor(.gray)
+ Spacer()
+ if !eta.isEmpty {
+ Text(eta)
+ .font(.caption)
+ .foregroundColor(.white.opacity(0.7))
+ }
+ }
+
+ HStack(spacing: 12) {
+ Image(systemName: Self.statusIcon(for: status))
+ .font(.title2)
+ .foregroundColor(Self.statusColor(for: status))
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(Self.statusLabel(for: status))
+ .font(.headline)
+ .foregroundColor(.white)
+ Text(message)
+ .font(.subheadline)
+ .foregroundColor(.white.opacity(0.8))
+ .lineLimit(1)
+ }
+ Spacer()
+ }
+
+ DeliveryProgressBar(status: status)
+ }
+ .padding()
+ .activityBackgroundTint(Color(red: 0.11, green: 0.13, blue: 0.19))
+ .activitySystemActionForegroundColor(.white)
+
+ } dynamicIsland: { context in
+ let status = context.state.data["status"]?.asString() ?? "preparing"
+ let message = context.state.data["message"]?.asString() ?? "Preparing"
+ let eta = context.state.data["estimatedTime"]?.asString() ?? ""
+
+ return DynamicIsland {
+ DynamicIslandExpandedRegion(.leading) {
+ Image(systemName: Self.statusIcon(for: status))
+ .font(.title2)
+ .foregroundColor(Self.statusColor(for: status))
+ }
+ DynamicIslandExpandedRegion(.center) {
+ Text(Self.statusLabel(for: status))
+ .font(.headline)
+ }
+ DynamicIslandExpandedRegion(.trailing) {
+ if !eta.isEmpty {
+ Text(eta)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ DynamicIslandExpandedRegion(.bottom) {
+ Text(message)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ } compactLeading: {
+ Image(systemName: Self.statusIcon(for: status))
+ .foregroundColor(Self.statusColor(for: status))
+ } compactTrailing: {
+ Text(Self.statusLabel(for: status))
+ .font(.caption)
+ } minimal: {
+ Image(systemName: Self.statusIcon(for: status))
+ .foregroundColor(Self.statusColor(for: status))
+ }
+ }
+ }
+
+ // MARK: - Status helpers
+
+ private static func statusIcon(for status: String) -> String {
+ switch status {
+ case "on_the_way": return "box.truck.fill"
+ case "delivered": return "checkmark.circle.fill"
+ default: return "bag.fill"
+ }
+ }
+
+ private static func statusColor(for status: String) -> Color {
+ switch status {
+ case "on_the_way": return .blue
+ case "delivered": return .green
+ default: return .orange
+ }
+ }
+
+ private static func statusLabel(for status: String) -> String {
+ switch status {
+ case "on_the_way": return "On the Way"
+ case "delivered": return "Delivered"
+ default: return "Preparing"
+ }
+ }
+}
+
+@available(iOS 16.2, *)
+struct DeliveryProgressBar: View {
+ let status: String
+
+ private var progress: CGFloat {
+ switch status {
+ case "on_the_way": return 0.6
+ case "delivered": return 1.0
+ default: return 0.25
+ }
+ }
+
+ var body: some View {
+ GeometryReader { geo in
+ ZStack(alignment: .leading) {
+ RoundedRectangle(cornerRadius: 3)
+ .fill(Color.white.opacity(0.2))
+ .frame(height: 6)
+ RoundedRectangle(cornerRadius: 3)
+ .fill(progress >= 1.0 ? Color.green : Color.blue)
+ .frame(width: geo.size.width * progress, height: 6)
+ }
+ }
+ .frame(height: 6)
+ }
+}
diff --git a/examples/demo/README.md b/examples/demo/README.md
new file mode 100644
index 000000000..42e5d5513
--- /dev/null
+++ b/examples/demo/README.md
@@ -0,0 +1,131 @@
+# OneSignal SwiftUI Example App
+
+A SwiftUI demo app that exercises every public surface of the OneSignal iOS SDK and mirrors the layout, naming, and behavior of other OneSignal SDKS so the same end-to-end test suite (`@onesignal/sdk-shared`) can drive both apps.
+
+## Features
+
+The demo covers all major OneSignal SDK capabilities:
+
+- **App / Consent**: App ID display, `consent_required` and `privacy_consent` toggles
+- **User**: Login / logout with external user ID
+- **Push Subscription**: Push subscription ID, opt-in toggle, prompt for permission
+- **Send Push Notification**: Simple / image / sound / custom notifications via the OneSignal REST API
+- **In-App Messaging**: Pause / resume IAM display, send IAM trigger to surface dashboard messages
+- **Aliases / Emails / SMS / Tags**: Add (single or multiple), remove (single or selected)
+- **Outcomes**: Send normal / unique / value outcomes
+- **Triggers**: Add (single or multiple), remove (single or selected), clear all
+- **Custom Events**: Track event with optional JSON properties
+- **Location**: Location sharing toggle, request permission
+- **Live Activities** (iOS 16.1+): Start / update / end an activity, status cycler
+
+Section headers use ALL CAPS and an info icon (where Capacitor has one) that opens a tooltip sheet with descriptions sourced from `https://github.com/OneSignal/sdk-shared` (with a bundled fallback).
+
+Every interactive element exposes an `accessibilityIdentifier` matching the Capacitor demo's `data-testid` so the shared E2E tests can target it.
+
+## Architecture
+
+The Xcode project ships three targets, mirroring the Capacitor / Cordova / RN demos:
+
+```
+examples/demo/
+├── App.xcodeproj
+├── App.entitlements # main app: aps-environment + app group
+├── App/ # Main app target source
+│ ├── App.swift # @main + AppDelegate, SDK + Live Activity setup
+│ ├── Views/
+│ │ ├── ContentView.swift # Composes sections + sheets in Capacitor order
+│ │ ├── Sections/ # AppSection, UserSection, PushSection, ...
+│ │ └── Components/ # SectionCard, ActionButton, ToggleRow,
+│ │ # AddItemSheet, MultiPairInputSheet, RemoveMultiSheet,
+│ │ # OutcomeSheet, CustomNotificationSheet, TrackEventSheet,
+│ │ # TooltipSheet, ToastView, ListWidgets, KeyValueRow
+│ ├── ViewModels/
+│ │ └── OneSignalViewModel.swift # Single ObservableObject backing every section
+│ ├── Models/
+│ │ └── AppModels.swift # KeyValueItem, NotificationType, InAppMessageType,
+│ │ # AddItemType, MultiAddItemType, RemoveMultiItemType,
+│ │ # OutcomeMode, TooltipData, UserData
+│ ├── Services/
+│ │ ├── OneSignalService.swift # Thin wrapper over OneSignal.* APIs
+│ │ ├── NotificationSender.swift # Posts to /notifications with retry on transient failures
+│ │ ├── UserFetchService.swift # Hydrates aliases / tags / channels via /users
+│ │ ├── TooltipService.swift # Loads tooltip JSON from sdk-shared (with fallback)
+│ │ └── LiveActivityController.swift # Wraps OneSignal.LiveActivities + REST update / end
+│ ├── Assets.xcassets/
+│ └── Info.plist
+│
+├── OneSignalNotificationServiceExtension/ # NSE target — required for rich push (images, decryption, mutable content)
+│ ├── NotificationService.swift # Forwards to OneSignalExtension.didReceiveNotificationExtensionRequest
+│ ├── Info.plist # NSExtension/usernotifications.service
+│ └── OneSignalNotificationServiceExtension.entitlements # app group (must match main app)
+│
+└── OneSignalWidget/ # Widget Extension target — required to render Live Activities
+ ├── OneSignalWidgetBundle.swift # @main WidgetBundle
+ ├── OneSignalWidgetLiveActivity.swift # Lock screen + Dynamic Island UI for DefaultLiveActivityAttributes
+ ├── Info.plist # NSExtension/widgetkit-extension
+ └── Assets.xcassets/ # WidgetBackground, AccentColor, AppIcon
+```
+
+This mirrors the Capacitor demo's iOS layout (`OneSignal-Capacitor-SDK/examples/demo/ios/App/{App,OneSignalNotificationServiceExtension,OneSignalWidget}/`).
+
+## Setup Instructions
+
+The Xcode project is generated from `project.yml` with [XcodeGen](https://github.com/yonaskolb/XcodeGen) and is wired into `iOS_SDK/OneSignalSDK.xcworkspace`, so it builds against the SDK source tree directly. There are no manual Xcode setup steps.
+
+### 1. Open the workspace
+
+```bash
+open iOS_SDK/OneSignalSDK.xcworkspace
+```
+
+In the scheme picker pick **App** and run on a simulator or device. Granting notification permissions and selecting a section is enough to exercise the SDK against your local source.
+
+### 2. Regenerate the project (only when `project.yml` changes)
+
+```bash
+brew install xcodegen # one time
+cd examples/demo
+xcodegen generate # rewrites App.xcodeproj
+```
+
+`project.yml` declares three targets — `App`, `OneSignalNotificationServiceExtension`, `OneSignalWidget` — and references the framework targets in `iOS_SDK/OneSignalSDK/OneSignal.xcodeproj` so each one links and embeds the right SDK frameworks at build time.
+
+### 3. Capabilities & App Group
+
+The shipped `App.entitlements` and `OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements` use `group.com.onesignal.example.onesignal`. If you need a different group (for example to install on a real device under your own team), change the value in both files to the same string. The other capabilities (Push Notifications, Remote notifications background mode, `NSSupportsLiveActivities`) are already declared in the entitlements / `App/Info.plist`.
+
+### 4. Configure your OneSignal credentials
+
+Both the App ID and (optional) REST API key live in a single `Secrets.plist` next to `App/Info.plist`. The file is gitignored. Add it to the App target with the keys you need:
+
+```xml
+
+ ONESIGNAL_APP_ID
+ YOUR_APP_ID
+ ONESIGNAL_API_KEY
+ YOUR_REST_API_KEY
+
+```
+
+- `ONESIGNAL_APP_ID` — optional. Falls back to the placeholder in `SecretsConfig.defaultAppId` when missing.
+- `ONESIGNAL_API_KEY` — optional, only needed for Live Activity **Update** / **End**. Without it, those buttons disable themselves and the section shows a hint.
+
+> The widget renders `DefaultLiveActivityAttributes` (provided by the SDK), so the Activity ID + Order # you type into the demo flows through to the same widget regardless of whether the update came from `OneSignal.LiveActivities` locally or from the REST `/live_activities/{id}/notifications` endpoint.
+
+## Running the App
+
+1. Select a simulator or device
+2. Build and run (⌘R)
+3. Grant notification permissions when prompted
+4. Explore each section
+
+## Requirements
+
+- iOS 15.0+ (Live Activities require iOS 16.1+)
+- Xcode 15.0+
+- Swift 5.9+
+- OneSignal iOS SDK 5.0+
+
+## License
+
+Modified MIT License — see the repository LICENSE file.
diff --git a/examples/demo/build.md b/examples/demo/build.md
new file mode 100644
index 000000000..cfb7e6302
--- /dev/null
+++ b/examples/demo/build.md
@@ -0,0 +1,353 @@
+# OneSignal iOS Sample App - Build Guide
+
+This document extends the shared build guide with iOS-specific details.
+
+**Read the shared guide first:**
+https://raw.githubusercontent.com/OneSignal/sdk-shared/refs/heads/main/demo/build.md
+
+Replace `{{PLATFORM}}` with `iOS` everywhere in that guide. Everything below either overrides or supplements sections from the shared guide.
+
+---
+
+## Project Setup
+
+The demo lives at `examples/demo/` (relative to the SDK repo root) and is wired into the same Xcode workspace as the SDK source (`iOS_SDK/OneSignalSDK.xcworkspace`), so it builds against your local SDK tree directly — no tarball, CocoaPods, or SPM package reference required.
+
+`App.xcodeproj` is generated from `project.yml` with [XcodeGen](https://github.com/yonaskolb/XcodeGen):
+
+```bash
+brew install xcodegen # one time
+cd examples/demo
+xcodegen generate # regenerates App.xcodeproj from project.yml
+```
+
+`project.yml` declares three targets and links them against the SDK framework targets defined in `iOS_SDK/OneSignalSDK/OneSignal.xcodeproj` via a `projectReferences` entry:
+
+- **App** — main app target, embeds and signs every public SDK framework (`OneSignalCore`, `OneSignalOSCore`, `OneSignalOutcomes`, `OneSignalNotifications`, `OneSignalUser`, `OneSignalExtension`, `OneSignalLocation`, `OneSignalInAppMessages`, `OneSignalLiveActivities`, `OneSignalFramework`) plus the two local extensions
+- **OneSignalNotificationServiceExtension** — links (does NOT embed) `OneSignalCore`, `OneSignalOutcomes`, `OneSignalExtension`
+- **OneSignalWidget** — links (does NOT embed) `OneSignalLiveActivities`
+
+Open `iOS_SDK/OneSignalSDK.xcworkspace`, select the **App** scheme, and run. The app and both extensions build from local SDK source, so SDK edits flow through immediately.
+
+### App icons
+
+`App/Assets.xcassets/AppIcon.appiconset/` ships pre-populated with the OneSignal logo asset. The widget extension has its own `OneSignalWidget/Assets.xcassets/AppIcon.appiconset/` plus an `AccentColor` and `WidgetBackground` color set (referenced via `ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME` / `ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME` in `project.yml`). No regeneration step is needed.
+
+### Environment / secrets
+
+Per-developer build settings and OneSignal credentials are split across two layers, both wired into XcodeGen via `project.yml`:
+
+- `Build.xcconfig` -- root xcconfig referenced by every target's `configFiles: { Debug, Release }`. It only does `#include? "Local.xcconfig"`, so a fresh clone builds with no extra setup.
+- `Local.xcconfig` (gitignored) -- per-developer overrides such as `DEVELOPMENT_TEAM`, `CODE_SIGN_STYLE`, and `PROVISIONING_PROFILE_SPECIFIER`. Anything set here survives `xcodegen generate`. See `Local.xcconfig.example` for the template.
+- `App/Secrets.plist` (gitignored, `optional: true` in `project.yml`) -- `` with `ONESIGNAL_APP_ID` and `ONESIGNAL_API_KEY` strings. Parsed at runtime by `App/Services/SecretsConfig.swift`. Bundled via an explicit `buildPhase: resources` source entry because XcodeGen otherwise treats `.plist` files as Info.plist-like and skips Copy Bundle Resources.
+- `App/vine_boom.wav` (gitignored optional) -- bundled custom notification sound; XcodeGen picks it up automatically from the `App/` source path.
+
+### Build & run
+
+There is no `setup.sh` / `run-ios.sh` script -- Xcode handles everything:
+
+1. `open iOS_SDK/OneSignalSDK.xcworkspace`
+2. Pick the **App** scheme
+3. ⌘R to build and run on the selected simulator or device
+
+The only manual step is running `xcodegen generate` after editing `project.yml`.
+
+---
+
+## State Management
+
+Use a single `OneSignalViewModel` (`App/ViewModels/OneSignalViewModel.swift`) as the central state manager. There is no repository wrapper -- the view-model calls the OneSignal SDK directly through a thin `OneSignalService` singleton (`App/Services/OneSignalService.swift`).
+
+- `@MainActor final class OneSignalViewModel: ObservableObject` with `@Published` properties for reactive state (app id, push subscription id, aliases, tags, emails, SMS, triggers, consent, location, IAM paused, `isLoading`)
+- The only `@Published` UI overlay state is `activeTooltip: TooltipData?`. Action dialogs are NOT in the view-model -- each section owns its own `@State` boolean and binds `.osCenteredDialog(isPresented:)` locally
+- `init` calls `refreshState()` and the private `setupObservers()`, which registers `OSPushSubscriptionObserver`, `OSUserStateObserver`, and `OSNotificationPermissionObserver`. SwiftUI keeps the view-model alive for the app lifetime via `@StateObject` in `App.swift`, so no manual teardown is needed
+- `OneSignalService` (singleton, `App/Services/OneSignalService.swift`) funnels every SDK call through one entry point and mirrors any setters the SDK doesn't expose as getters (consent flags) into `UserDefaults`
+- `NotificationSender` (singleton, `App/Services/NotificationSender.swift`) wraps the `/notifications` REST endpoint with `URLSession` and retries with exponential backoff when the API returns 200 with empty `id` / `recipients == 0` / a non-empty `errors` payload (transient race between subscription create and notification fan-out)
+- `UserFetchService` (singleton, `App/Services/UserFetchService.swift`) hydrates aliases / tags / emails / SMS via `GET /users/by/onesignal_id/{id}` -- no auth header, public endpoint
+- `LiveActivityController` (`App/Services/LiveActivityController.swift`) wraps `OneSignal.LiveActivities.startDefault(...)` plus the authenticated REST update/end calls. Reads the API key via `SecretsConfig.apiKey`; missing/empty key disables UPDATE / END
+- `SecretsConfig` (`App/Services/SecretsConfig.swift`) reads `ONESIGNAL_APP_ID` and `ONESIGNAL_API_KEY` from a bundled `Secrets.plist` (iOS equivalent of `.env`); both keys optional, app ID falls back to a placeholder when missing
+- `TooltipService` (singleton, `App/Services/TooltipService.swift`) loads the shared tooltip JSON from `sdk-shared` on a detached task with a bundled fallback so the first render isn't blocked
+
+### SDK initialization
+
+`AppDelegate.application(_:didFinishLaunchingWithOptions:)` in `App/App.swift` is intentionally minimal:
+
+```swift
+OneSignalService.shared.initialize(launchOptions: launchOptions)
+setupNotificationListeners()
+setupInAppMessageListeners()
+if #available(iOS 16.1, *) { LiveActivityController.setup() }
+```
+
+- `OneSignalService.initialize(launchOptions:)` mirrors the Capacitor `useOneSignal` startup order so toggles persist across cold launches:
+ 1. `OneSignal.Debug.setLogLevel(.LL_VERBOSE)`
+ 2. `OneSignal.setConsentRequired(prefs.getConsentRequired())`
+ 3. `OneSignal.setConsentGiven(prefs.getConsentGiven())`
+ 4. `OneSignal.initialize(appId, withLaunchOptions:)`
+ 5. `OneSignal.InAppMessages.paused = prefs.getIamPaused()`
+ 6. `OneSignal.Location.isShared = prefs.getLocationShared()`
+ 7. If `prefs.getExternalUserId()` is non-nil: `OneSignal.login(storedExternalId)`
+- `LiveActivityController.setup()` wraps `OneSignal.LiveActivities.setupDefault()` (iOS 16.1+ guard lives in the controller, not inline)
+- The four SDK listeners (`NotificationLifecycleHandler`, `NotificationClickHandler`, `InAppMessageLifecycleHandler`, `InAppMessageClickHandler`) are registered via `OneSignal.Notifications.add*Listener(...)` / `OneSignal.InAppMessages.add*Listener(...)` from the `setupNotificationListeners` / `setupInAppMessageListeners` helpers
+
+`PreferencesService` (`App/Services/PreferencesService.swift`) is the demo's UserDefaults-backed cache, keyed under `onesignal.demo.*`. It's the single source of truth for any state the demo needs to restore on a fresh launch: consent flags, IAM-paused, location-shared, and the last-logged-in external user id. Setters on `OneSignalService` read-through and write-through this cache (in addition to forwarding to the SDK), so the view model's `@Published` props can hydrate from `service.consentRequired` / `service.consentGiven` / `service.isInAppMessagesPaused` / `service.isLocationShared` and get cached values on cold launch.
+
+Push subscription id, opt-in, notification permission, and the live `OneSignal.User.externalId` are still read directly from the SDK at runtime (they don't need preference caching).
+
+---
+
+## iOS-Specific UI Details
+
+### Notification Permission
+
+- `OneSignalViewModel` exposes `promptPushPermission()` (no `isReady` gate, no separate `promptPush()` method)
+- `ContentView` auto-prompts on first appear via an unconditional `.task { viewModel.promptPushPermission() }` modifier on the root view -- this races the OneSignal iOS-params response so the standard alert shows before the SDK can register provisional auth
+- `PushSection` renders a conditional `PROMPT PUSH` button that calls `viewModel.promptPushPermission()`. The button is hidden once `hasNotificationPermission == true`
+
+### Loading State
+
+- `isLoading` is currently dead state in the view model -- it's flipped inside `fetchUserDataFromApi()` and `login(externalId:)` but no file under `App/Views/` references it. The Aliases / Emails / SMS / Tags sections always render their static empty-state copy via `PairList` / `SingleList` regardless of fetch state
+- Stale-result protection: `fetchUserDataFromApi()` increments a `requestSequence` counter on entry, captures the value, and short-circuits after the `await` if a newer fetch has run in the meantime. Mirrors the `requestSequenceRef` pattern from the Capacitor demo so back-to-back logout / login flows don't get overwritten by a slow earlier fetch
+
+### Toast
+
+- `ToastPresenter` (`App/ViewModels/ToastPresenter.swift`) is a `@MainActor` `ObservableObject` with `@Published var message: String?` and a `show(_:)` method. It is created as a `@StateObject` in `App.swift` and injected into `ContentView` via `.environmentObject(toastPresenter)`.
+- Section views declare `@EnvironmentObject var toast: ToastPresenter` and call `toast.show(...)` from action handlers. Only Outcomes, Custom Events, and Location check trigger the toast; everything else uses `print()` only.
+- `ContentView` attaches the host `.toast(message: $toast.message)` modifier (defined in `App/Views/Components/ToastView.swift`) so a single host renders the current message regardless of which section emitted it.
+- Replace-on-show: `show(_:)` cancels the previous `dismissTask`, sets `self.message`, and starts a new `Task` that sleeps `ToastPresenter.toastDurationMs` (milliseconds) and clears `message` only if it still matches the captured target string.
+- Duration is the static constant `static let toastDurationMs: UInt64 = 3_000` (milliseconds).
+- `OneSignalViewModel` must not hold any toast state, expose `toastMessage`, or call a `showToast` method.
+
+### Dialogs
+
+- Tooltip state lives on the view model as `@Published var activeTooltip: TooltipData?`. `ContentView` owns layout only and binds the tooltip dialog via `viewModel.activeTooltip` / `viewModel.dismissTooltip()` attached with `.osCenteredDialog`. Sections call `viewModel.showTooltip(for:)` from info icons.
+- Sections declare `@State` booleans for their action dialogs (`@State private var addOpen = false`, `@State private var loginOpen = false`, ...) and attach `.osCenteredDialog(isPresented: $addOpen) { AddItemDialog(...) }` on the section view. Dialog confirm handlers call ViewModel SDK methods and (where applicable) `toast.show(...)`.
+- `OneSignalViewModel` must not hold any action dialog visibility flags or dialog input drafts.
+- `osCenteredDialog` (in `App/Views/Components/OSDialog.swift`) is implemented on top of `.fullScreenCover` with a `ClearBackgroundView` (`UIViewRepresentable`) so the dialog presents at the window level instead of being clipped to the section's frame inside `ScrollView`. The default slide-up animation is suppressed via `.transaction { $0.disablesAnimations = true }` so the dialog's own fade-in is preserved.
+- Shared dialog primitives live in `App/Views/Components/`: `AddItemDialog` (typed via `AddItemType` -- single-field and pair layouts both flow through it), `MultiPairInputDialog`, `RemoveMultiDialog`, `OutcomeDialog`, `CustomNotificationDialog`, `TrackEventDialog`, `TooltipDialog`. Sections import and compose them locally.
+
+### Accessibility (Appium)
+
+Apply test ids with SwiftUI's `.accessibilityIdentifier("…")` modifier on every interactive element and value display. The ids match the `data-testid` values used by the Capacitor / React Native / Cordova demos one-for-one so the shared Appium suite under `sdk-shared/appium/tests/` runs unchanged against the iOS build.
+
+XCUITest does NOT inherit identifiers from `Button(role:)` automatically -- set `.accessibilityIdentifier(...)` on every `Button`, `Toggle`, `TextField`, and the wrapping `VStack` of each section.
+
+- `ContentView` anchors `accessibilityIdentifier("main_scroll_view")` on the SwiftUI `ScrollView` itself (not the inner `VStack`) so XCUITest exposes it as `XCUIElementTypeScrollView` with the visible viewport's rect. The shared Appium swipe workaround on iOS depends on this anchoring -- attaching the id to the inner stack reports the full content rect (multiple screens tall) and WDIO `swipe` then computes gestures outside the viewport, which iOS clips to the visible region and registers as taps on whatever button sits there.
+- `ContentView` runs the auto push-permission prompt via `.task { viewModel.promptPushPermission() }` on mount. It races the OneSignal iOS-params response, so the standard alert can show before the SDK registers provisional auth (which would otherwise silently grant permission and skip the prompt entirely).
+
+### Branding assets
+
+`App/Assets.xcassets/` ships three branded asset folders alongside the standard `AppIcon` / `AccentColor`:
+
+- `LaunchBackground.colorset` -- referenced by `UILaunchScreen.UIColorName` in `App/Info.plist`
+- `onesignal_launch_icon.imageset` -- referenced by `UILaunchScreen.UIImageName`
+- `onesignal_logo.imageset` -- rendered as a template image in the `ContentView` toolbar's principal placement
+
+---
+
+## Xcode Project Targets
+
+### Notification Service Extension
+
+`OneSignalNotificationServiceExtension/NotificationService.swift` forwards every push to `OneSignalExtension` so rich attachments (`ios_attachments`), confidential pushes, and `mutable_content` payloads work:
+
+```swift
+override func didReceive(_ request: UNNotificationRequest,
+ withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+ self.receivedRequest = request
+ self.contentHandler = contentHandler
+ self.bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
+
+ if let bestAttemptContent = bestAttemptContent {
+ OneSignalExtension.didReceiveNotificationExtensionRequest(
+ self.receivedRequest, with: bestAttemptContent, withContentHandler: contentHandler)
+ }
+}
+
+override func serviceExtensionTimeWillExpire() {
+ if let contentHandler, let bestAttemptContent {
+ OneSignalExtension.serviceExtensionTimeWillExpireRequest(receivedRequest, with: bestAttemptContent)
+ contentHandler(bestAttemptContent)
+ }
+}
+```
+
+The NSE entitlements file (`OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements`) **must** declare the same `com.apple.security.application-groups` value as the main app — both ship with `group.com.onesignal.example.onesignal`. If you change the group to install on a real device under your own team, change it in BOTH files to the same string.
+
+### Widget Extension (Live Activities)
+
+`OneSignalWidget/OneSignalWidgetLiveActivity.swift` renders the order tracking flow using `DefaultLiveActivityAttributes` from `OneSignalLiveActivities`. Replace the file with the shared reference implementation at `https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/LiveActivity.swift` whenever the canonical version is updated.
+
+The widget target's deployment target is `16.2` (project-wide is `16.0`) because Dynamic Island APIs require 16.2. `NSSupportsLiveActivities = true` is declared in `App/Info.plist`.
+
+---
+
+## Platform Config
+
+### Entitlements
+
+`App.entitlements` (main app):
+
+```xml
+aps-environment
+development
+com.apple.security.application-groups
+
+ group.com.onesignal.example.onesignal
+
+```
+
+`OneSignalNotificationServiceExtension.entitlements` mirrors the same app group. Both must match or rich pushes fail silently.
+
+### Info.plist
+
+`App/Info.plist` declares:
+
+- `NSLocationWhenInUseUsageDescription` and `NSLocationAlwaysAndWhenInUseUsageDescription` -- required for the Location section's prompt
+- `NSSupportsLiveActivities = true` -- required for the Live Activity section
+- `NSSupportsLiveActivitiesFrequentUpdates = true` -- enables high-frequency push updates to running activities
+- `UIBackgroundModes` with `remote-notification` -- required for silent / background pushes
+- `UILaunchScreen` references the `LaunchBackground` color set and `onesignal_launch_icon` image set bundled in `App/Assets.xcassets/`
+
+### Custom Notification Sound
+
+The demo bundles `examples/demo/App/vine_boom.wav` (sourced from [sdk-shared/assets](https://github.com/OneSignal/sdk-shared/tree/main/assets)). XcodeGen picks it up automatically via the `sources: - path: App` block, and `NotificationSender.swift`'s WITH SOUND payload sets `ios_sound = "vine_boom.wav"` to play it.
+
+### Credentials (App ID & REST API key)
+
+The iOS demo does NOT use a `.env` file. Instead, `App/Services/SecretsConfig.swift` reads both `ONESIGNAL_APP_ID` and `ONESIGNAL_API_KEY` from a single `Secrets.plist` bundled with the App target — the iOS-idiomatic equivalent of `.env`:
+
+```xml
+
+ ONESIGNAL_APP_ID
+ YOUR_APP_ID
+ ONESIGNAL_API_KEY
+ YOUR_REST_API_KEY
+
+```
+
+- `ONESIGNAL_APP_ID` — optional. Falls back to `SecretsConfig.defaultAppId` (the placeholder defined in `sdk-shared/demo/build.md`) when missing or empty. `OneSignalService.shared.appId` is captured from `SecretsConfig.appId` once during `init`, so the value is stable for the running session.
+- `ONESIGNAL_API_KEY` — optional, only needed for Live Activity **update** / **end**. `LiveActivityController.hasApiKey` is `true` when set; otherwise the UPDATE / END buttons disable themselves and show a hint in the Live Activity section.
+
+`Secrets.plist` is gitignored.
+
+---
+
+## File Structure
+
+```
+examples/demo/
+├── App.xcodeproj # Generated by `xcodegen generate`
+├── project.yml # XcodeGen project definition
+├── App.entitlements # aps-environment + app group
+├── Build.xcconfig # Root xcconfig wired into every target;
+│ # only does `#include? "Local.xcconfig"`
+├── Local.xcconfig.example # Per-developer overrides template
+│ # (DEVELOPMENT_TEAM, CODE_SIGN_STYLE, ...)
+├── build.md # This file
+├── README.md
+├── App/ # Main app target source
+│ ├── App.swift # @main + AppDelegate; calls
+│ │ # OneSignalService.shared.initialize,
+│ │ # registers NotificationLifecycleHandler /
+│ │ # NotificationClickHandler /
+│ │ # InAppMessageLifecycleHandler /
+│ │ # InAppMessageClickHandler, runs
+│ │ # LiveActivityController.setup() on iOS 16.1+
+│ ├── Info.plist
+│ ├── Secrets.plist # gitignored optional; ONESIGNAL_APP_ID +
+│ │ # ONESIGNAL_API_KEY consumed by SecretsConfig
+│ ├── vine_boom.wav # gitignored optional; custom notification sound
+│ ├── Assets.xcassets/
+│ │ ├── AppIcon.appiconset/
+│ │ ├── AccentColor.colorset/
+│ │ ├── LaunchBackground.colorset/ # UILaunchScreen background color
+│ │ ├── onesignal_launch_icon.imageset/ # UILaunchScreen image
+│ │ └── onesignal_logo.imageset/ # Used by ContentView toolbar
+│ ├── Models/
+│ │ └── AppModels.swift # KeyValueItem, NotificationType,
+│ │ # AddItemType, MultiAddItemType,
+│ │ # RemoveMultiItemType, OutcomeMode,
+│ │ # TooltipData, UserData
+│ ├── ViewModels/
+│ │ ├── OneSignalViewModel.swift # @MainActor ObservableObject, holds
+│ │ │ # @Published activeTooltip, drives REST
+│ │ │ # fetches via UserFetchService, registers
+│ │ │ # SDK observers via private setupObservers()
+│ │ └── ToastPresenter.swift # @MainActor ObservableObject; @Published
+│ │ # message + show() with replace-on-show
+│ ├── Services/
+│ │ ├── OneSignalService.swift # Thin wrapper over OneSignal.* APIs
+│ │ ├── SecretsConfig.swift # Reads ONESIGNAL_APP_ID / ONESIGNAL_API_KEY
+│ │ │ # from Secrets.plist with defaults
+│ │ ├── NotificationSender.swift # /notifications POST + transient-retry loop
+│ │ ├── UserFetchService.swift # /users GET, parses identity + tags + subs
+│ │ ├── TooltipService.swift # Loads sdk-shared tooltip JSON (with fallback)
+│ │ └── LiveActivityController.swift # OneSignal.LiveActivities + REST update/end
+│ └── Views/
+│ ├── ContentView.swift # NavigationStack + ScrollView; layout +
+│ │ # auto push-permission `.task` + tooltip dialog
+│ │ # via viewModel.activeTooltip; sections own
+│ │ # action dialog state
+│ ├── Theme.swift # Design tokens from sdk-shared/demo/styles.md
+│ ├── Sections/
+│ │ ├── AppSection.swift
+│ │ ├── UserSection.swift
+│ │ ├── PushSection.swift
+│ │ ├── SendPushSection.swift
+│ │ ├── InAppSection.swift
+│ │ ├── SendIamSection.swift
+│ │ ├── AliasesSection.swift
+│ │ ├── EmailsSection.swift
+│ │ ├── SmsSection.swift
+│ │ ├── TagsSection.swift
+│ │ ├── OutcomesSection.swift
+│ │ ├── TriggersSection.swift
+│ │ ├── CustomEventsSection.swift
+│ │ ├── LocationSection.swift
+│ │ └── LiveActivitySection.swift
+│ └── Components/
+│ ├── SectionCard.swift
+│ ├── ActionButton.swift
+│ ├── ToggleRow.swift
+│ ├── ListWidgets.swift # PairList + SingleList; private helpers
+│ │ # ListCardEmpty, ItemDivider, DeleteButton,
+│ │ # MoreLink. No LoadingState / CollapsibleList
+│ ├── KeyValueRow.swift # Filename vs type name differ -- type is
+│ │ # `InfoRow` (currently unused in demo)
+│ ├── OSDialog.swift # osCenteredDialog modifier + ClearBackgroundView
+│ ├── AddItemDialog.swift # Single + Pair input dialogs (typed via AddItemType)
+│ ├── MultiPairInputDialog.swift # Bulk add (aliases / tags / triggers)
+│ ├── RemoveMultiDialog.swift # Bulk remove (tags / triggers)
+│ ├── OutcomeDialog.swift # Normal / Unique / With Value
+│ ├── CustomNotificationDialog.swift
+│ ├── TrackEventDialog.swift # Name + JSON properties, validates JSON
+│ ├── TooltipDialog.swift
+│ └── ToastView.swift # toast(message:) host modifier
+│
+├── OneSignalNotificationServiceExtension/ # NSE target -- rich push
+│ ├── NotificationService.swift # Forwards to OneSignalExtension
+│ ├── Info.plist # NSExtension/usernotifications.service
+│ └── OneSignalNotificationServiceExtension.entitlements # MUST match main app group
+│
+└── OneSignalWidget/ # Widget Extension target -- Live Activities
+ ├── OneSignalWidgetBundle.swift # @main WidgetBundle
+ ├── OneSignalWidgetLiveActivity.swift # Lock screen + Dynamic Island UI for
+ │ # DefaultLiveActivityAttributes
+ ├── Info.plist # NSExtension/widgetkit-extension
+ └── Assets.xcassets/ # WidgetBackground, AccentColor, AppIcon
+```
+
+---
+
+## iOS Best Practices
+
+- Re-run `xcodegen generate` after any change to `project.yml` so `App.xcodeproj` stays in sync. Commit the regenerated project file with the YAML change.
+- Always link the SDK frameworks through the workspace's `projectReferences` (not via SPM or CocoaPods inside the demo) so the demo builds against your local SDK edits without an extra publish step.
+- Keep the app group string identical in `App.entitlements` AND `OneSignalNotificationServiceExtension.entitlements` — they MUST match for confidential pushes and badge sync.
+- Embed and code-sign each SDK framework on the App target only; the NSE and Widget targets must link the frameworks they need without embedding (the App target owns them in `Frameworks/`).
+- Consent / IAM-paused / location-shared restore is NOT implemented in `App.swift` today. The view model only tracks UI toggle state in `Cached*` UserDefaults keys; the SDK side mirrors its own writes through separate `OneSignal*` UserDefaults keys via `OneSignalService`, and the two key sets are not synced. The SDK is the source of truth for everything else (push subscription id, external id, permission, tags) -- read it directly instead of caching.
+- Use `OneSignal.User.pushSubscription.optIn()` / `optOut()` rather than touching `optedIn` directly; the SDK applies side effects (token registration, server sync) inside the methods.
+- Drive `fetchUserDataFromApi` from the `OSUserStateObserver` only — never call it synchronously right after `OneSignal.login(...)`. The SDK assigns the new `onesignalId` asynchronously, so a synchronous fetch races the assignment and returns null.
+- Set `.accessibilityIdentifier(...)` on every interactive control and value display you want to drive from Appium / XCUITest. SwiftUI does not derive identifiers from button titles, and the shared E2E suite selects by identifier.
+- Bundle `Secrets.plist` with the App target for the Live Activity REST calls; without it the section disables UPDATE / END instead of failing at runtime.
diff --git a/examples/demo/project.yml b/examples/demo/project.yml
new file mode 100644
index 000000000..fe654b541
--- /dev/null
+++ b/examples/demo/project.yml
@@ -0,0 +1,191 @@
+name: App
+options:
+ bundleIdPrefix: com.onesignal.example
+ deploymentTarget:
+ iOS: "16.0"
+ developmentLanguage: en
+ createIntermediateGroups: true
+ generateEmptyDirectories: false
+ groupSortPosition: top
+configs:
+ Debug: debug
+ Release: release
+settings:
+ base:
+ SWIFT_VERSION: "5.9"
+ CURRENT_PROJECT_VERSION: "1"
+ MARKETING_VERSION: "1.0"
+ CODE_SIGN_STYLE: Automatic
+ DEVELOPMENT_TEAM: ""
+ GENERATE_INFOPLIST_FILE: NO
+projectReferences:
+ OneSignalSDK:
+ path: ../../iOS_SDK/OneSignalSDK/OneSignal.xcodeproj
+targets:
+ # ---------------------------------------------------------------------------
+ # Main app
+ # ---------------------------------------------------------------------------
+ App:
+ type: application
+ platform: iOS
+ deploymentTarget: "16.0"
+ configFiles:
+ Debug: Build.xcconfig
+ Release: Build.xcconfig
+ sources:
+ - path: App
+ excludes:
+ - "Info.plist"
+ - "**/*.entitlements"
+ # Handled separately below so we can force buildPhase: resources.
+ # XcodeGen defaults .plist files to BuildPhase.none (assumes
+ # Info.plist-like), which leaves Secrets.plist out of Copy Bundle
+ # Resources and Bundle.main.url(...) returns nil at runtime.
+ - "Secrets.plist"
+ # Credentials read by App/Services/SecretsConfig.swift at runtime.
+ # Gitignored and auto-written by sdk-shared/appium/scripts/run-local.sh
+ # when ONESIGNAL_APP_ID / ONESIGNAL_API_KEY are set; manually populated
+ # otherwise (see README step 4). optional: true so xcodegen succeeds on
+ # a fresh clone where the file hasn't been created yet.
+ - path: App/Secrets.plist
+ buildPhase: resources
+ optional: true
+ settings:
+ base:
+ PRODUCT_NAME: "$(TARGET_NAME)"
+ PRODUCT_BUNDLE_IDENTIFIER: com.onesignal.example
+ INFOPLIST_FILE: App/Info.plist
+ CODE_SIGN_ENTITLEMENTS: App.entitlements
+ TARGETED_DEVICE_FAMILY: "1,2"
+ LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks"
+ ENABLE_USER_SCRIPT_SANDBOXING: NO
+ configs:
+ Debug:
+ DEVELOPMENT_TEAM: 99SW8E36CT
+ Release:
+ CODE_SIGN_IDENTITY: "iPhone Developer"
+ CODE_SIGN_STYLE: Manual
+ DEVELOPMENT_TEAM: ""
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]": 99SW8E36CT
+ PROVISIONING_PROFILE_SPECIFIER: ""
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]": "Appium Demo - Main"
+ dependencies:
+ # SDK Swift framework targets (built from local source via the workspace)
+ - target: OneSignalSDK/OneSignalCore
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalOSCore
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalOutcomes
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalNotifications
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalUser
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalExtension
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalLocation
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalInAppMessages
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalLiveActivities
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalFramework
+ embed: true
+ codeSign: true
+ # Local app extensions
+ - target: OneSignalNotificationServiceExtension
+ embed: true
+ codeSign: true
+ - target: OneSignalWidget
+ embed: true
+ codeSign: true
+
+ # ---------------------------------------------------------------------------
+ # Notification Service Extension (rich push)
+ # ---------------------------------------------------------------------------
+ OneSignalNotificationServiceExtension:
+ type: app-extension
+ platform: iOS
+ deploymentTarget: "16.0"
+ configFiles:
+ Debug: Build.xcconfig
+ Release: Build.xcconfig
+ sources:
+ - path: OneSignalNotificationServiceExtension
+ excludes:
+ - "Info.plist"
+ - "**/*.entitlements"
+ settings:
+ base:
+ PRODUCT_NAME: "$(TARGET_NAME)"
+ PRODUCT_BUNDLE_IDENTIFIER: com.onesignal.example.NSE
+ INFOPLIST_FILE: OneSignalNotificationServiceExtension/Info.plist
+ CODE_SIGN_ENTITLEMENTS: OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements
+ TARGETED_DEVICE_FAMILY: "1,2"
+ LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"
+ SKIP_INSTALL: YES
+ configs:
+ Debug:
+ DEVELOPMENT_TEAM: 99SW8E36CT
+ Release:
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]": "iPhone Developer"
+ CODE_SIGN_STYLE: Manual
+ DEVELOPMENT_TEAM: ""
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]": 99SW8E36CT
+ PROVISIONING_PROFILE_SPECIFIER: ""
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]": "Appium Demo - NSE"
+ dependencies:
+ - target: OneSignalSDK/OneSignalCore
+ embed: false
+ - target: OneSignalSDK/OneSignalOutcomes
+ embed: false
+ - target: OneSignalSDK/OneSignalExtension
+ embed: false
+
+ # ---------------------------------------------------------------------------
+ # Widget Extension (Live Activities)
+ # ---------------------------------------------------------------------------
+ OneSignalWidget:
+ type: app-extension
+ platform: iOS
+ deploymentTarget: "16.2"
+ configFiles:
+ Debug: Build.xcconfig
+ Release: Build.xcconfig
+ sources:
+ - path: OneSignalWidget
+ excludes:
+ - "Info.plist"
+ - "**/*.entitlements"
+ settings:
+ base:
+ PRODUCT_NAME: "$(TARGET_NAME)"
+ PRODUCT_BUNDLE_IDENTIFIER: com.onesignal.example.LA
+ INFOPLIST_FILE: OneSignalWidget/Info.plist
+ TARGETED_DEVICE_FAMILY: "1,2"
+ LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"
+ SKIP_INSTALL: YES
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME: WidgetBackground
+ configs:
+ Debug:
+ DEVELOPMENT_TEAM: 99SW8E36CT
+ Release:
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]": "iPhone Developer"
+ CODE_SIGN_STYLE: Manual
+ DEVELOPMENT_TEAM: ""
+ "DEVELOPMENT_TEAM[sdk=iphoneos*]": 99SW8E36CT
+ PROVISIONING_PROFILE_SPECIFIER: ""
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]": "Appium Demo - Live Activity"
+ dependencies:
+ - target: OneSignalSDK/OneSignalLiveActivities
+ embed: false
diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.h b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.h
index 8ed7f14d9..a2cdb8caf 100644
--- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.h
+++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.h
@@ -29,9 +29,10 @@
// This project exisits to make testing OneSignal SDK changes.
#import
+#import
#import
-@interface AppDelegate : UIResponder
+@interface AppDelegate : UIResponder
@property (strong, nonatomic) UIWindow *window;
diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m
index 5de2efb52..7aabdb847 100644
--- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m
+++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m
@@ -50,9 +50,20 @@ @implementation AppDelegate
typedef void (^JwtCompletionBlock)(NSString*);
typedef void (^JwtExpiredBlock)(NSString *, JwtCompletionBlock);
+- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
+ // Log the full tracking URL and the original extracted URL
+ // Also trigger trackClickAndReturnOriginal twice to confirm this click event is only sent once
+ NSLog(@"Dev App: application openURL FULL URL is %@", url);
+ NSURL *originalURL1 = [OneSignal.LiveActivities trackClickAndReturnOriginal:url];
+ NSURL *originalURL2 = [OneSignal.LiveActivities trackClickAndReturnOriginal:url];
+ NSLog(@"Dev App: application openURL processed, original URL is %@", originalURL1);
+ return YES;
+}
+
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// [FIRApp configure];
+ [UNUserNotificationCenter currentNotificationCenter].delegate = self;
NSLog(@"Bundle URL: %@", [[NSBundle mainBundle] bundleURL]);
// Uncomment to test LogListener
@@ -196,17 +207,66 @@ - (void)applicationDidBecomeActive:(UIApplication *)application {
- (void)applicationWillTerminate:(UIApplication *)application {
}
-// Remote
+- (void)onLogEvent:(OneSignalLogEvent * _Nonnull)event {
+ NSLog(@"Dev App onLogEvent: %@", event.entry);
+}
+
+#pragma mark - Manual Integration APIs (for use when swizzling is disabled)
+
+// Forward the APNs device token to OneSignal so it can register the device for push
- (void)application:(UIApplication *)application
-didReceiveRemoteNotification:(NSDictionary *)userInfo
-fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
-
- NSLog(@"application:didReceiveRemoteNotification:fetchCompletionHandler: %@", userInfo);
- completionHandler(UIBackgroundFetchResultNoData);
+ didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
+ NSLog(@"Dev App application:didRegisterForRemoteNotificationsWithDeviceToken %@", deviceToken);
+ [OneSignal.Notifications didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
-- (void)onLogEvent:(OneSignalLogEvent * _Nonnull)event {
- NSLog(@"Dev App onLogEvent: %@", event.entry);
+// Forward APNs registration failures so OneSignal can log and retry appropriately
+- (void)application:(UIApplication *)application
+ didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
+ NSLog(@"Dev App application:didFailToRegisterForRemoteNotificationsWithError %@", error);
+ [OneSignal.Notifications didFailToRegisterForRemoteNotificationsWithError:error];
+}
+
+// Forward background / silent notifications for content-available processing
+- (void)application:(UIApplication *)application
+ didReceiveRemoteNotification:(NSDictionary *)userInfo
+ fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
+ NSLog(@"Dev App application:didReceiveRemoteNotification %@", userInfo);
+ [OneSignal.Notifications didReceiveRemoteNotification:userInfo
+ completionHandler:completionHandler];
+}
+
+// Forward foreground notifications so the SDK can invoke onWillDisplayNotification listeners
+// and determine whether to show a banner. Completion returns nil for IAM previews.
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center
+ willPresentNotification:(UNNotification *)notification
+ withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
+ NSLog(@"Dev App userNotificationCenter:willPresentNotification %@", notification);
+ [OneSignal.Notifications
+ willPresentNotificationWithPayload:notification.request.content.userInfo
+ completion:^(OSNotification *notif) {
+ if (notif) {
+ if (@available(iOS 14.0, *)) {
+ completionHandler(UNNotificationPresentationOptionBanner |
+ UNNotificationPresentationOptionList |
+ UNNotificationPresentationOptionSound);
+ } else {
+ completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound);
+ }
+ } else {
+ completionHandler(UNNotificationPresentationOptionNone);
+ }
+ }];
+}
+
+// Forward notification tap / action so the SDK can fire onClickNotification listeners
+// and handle deep links and action buttons
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center
+ didReceiveNotificationResponse:(UNNotificationResponse *)response
+ withCompletionHandler:(void (^)(void))completionHandler {
+ NSLog(@"Dev App userNotificationCenter:didReceiveNotificationResponse %@", response);
+ [OneSignal.Notifications didReceiveNotificationResponse:response];
+ completionHandler();
}
@end
diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist
index 7ac3fdb50..bd8031ea4 100644
--- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist
+++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Info.plist
@@ -24,6 +24,17 @@
APPL
CFBundleShortVersionString
$(MARKETING_VERSION)
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ com.onesignal.example
+ CFBundleURLSchemes
+
+ myapp
+
+
+
CFBundleVersion
$(CURRENT_PROJECT_VERSION)
LSRequiresIPhoneOS
@@ -71,5 +82,7 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ OneSignal_disable_swizzling
+
diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift
index 35d7de8fc..722cceb4b 100644
--- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift
+++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift
@@ -28,25 +28,54 @@
import Foundation
import OneSignalFramework
-class SwiftTest: NSObject, OSUserJwtInvalidatedListener, OSLogListener {
+class SwiftTest: NSObject, OSLogListener {
func onLogEvent(_ event: OneSignalLogEvent) {
print("Dev App onLogEvent: \(event.level) - \(event.entry)")
}
- func onUserJwtInvalidated(event: OSUserJwtInvalidatedEvent) {
- print("event: \(event.jsonRepresentation())")
- print("externalId: \(event.externalId)")
- }
-
func testSwiftUserModel() {
let token1 = OneSignal.User.pushSubscription.token
let token = OneSignal.User.pushSubscription.token
- OneSignal.Debug._dump()
- OneSignal.login(externalId: "euid", token: "token")
- OneSignal.updateUserJwt(externalId: "euid", token: "token")
- OneSignal.addUserJwtInvalidatedListener(self)
- OneSignal.removeUserJwtInvalidatedListener(self)
OneSignal.Debug.addLogListener(self)
OneSignal.Debug.removeLogListener(self)
}
+
+ /**
+ Track multiple events with different properties.
+ Properties must pass `JSONSerialization.isValidJSONObject` to be accepted.
+ */
+ @objc
+ static func trackCustomEvents() {
+ print("Dev App: track an event with nil properties")
+ OneSignal.User.trackEvent(name: "null properties", properties: nil)
+
+ print("Dev App: track an event with empty properties")
+ OneSignal.User.trackEvent(name: "empty properties", properties: [:])
+
+ let formatter = DateFormatter()
+ formatter.dateStyle = .short
+
+ let mixedTypes = [
+ "string": "somestring",
+ "number": 5,
+ "bool": false,
+ "dateStr": formatter.string(from: Date())
+ ] as [String: Any]
+
+ let nestedDict = [
+ "someDict": mixedTypes,
+ "anotherDict": [
+ "foo": "bar",
+ "booleanVal": true,
+ "float": Float("3.14")!
+ ]
+ ]
+ let invalidProperties = ["date": Date()]
+
+ print("Dev App: track an event with a valid nested dictionary")
+ OneSignal.User.trackEvent(name: "nested dictionary", properties: nestedDict)
+
+ print("Dev App: track an event with invalid dictionary types")
+ OneSignal.User.trackEvent(name: "invalid dictionary", properties: invalidProperties)
+ }
}
diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.h b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.h
index 6e778926d..a8ae2dd79 100644
--- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.h
+++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.h
@@ -80,6 +80,7 @@
@property (weak, nonatomic) IBOutlet UITextField *activityId;
@property (weak, nonatomic) IBOutlet UITextField *languageTextField;
+@property (weak, nonatomic) IBOutlet UITextField *customEventsTextField;
@end
diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m
index 6cb2e0edd..903fec7c8 100644
--- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m
+++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m
@@ -251,8 +251,12 @@ - (IBAction)startAndEnterLiveActivity:(id)sender {
NSString *activityId = [self.activityId text];
// Will not make a live activity if activityId is empty
if (activityId && activityId.length) {
-// [LiveActivityController createDefaultActivityWithActivityId:activityId ];
- [LiveActivityController createActivityWithActivityId:activityId completionHandler:^(void) {} ];
+ // 1. Create a Default activity
+ // [LiveActivityController createDefaultActivityWithActivityId:activityId ];
+ // 2. Create non-OneSignal-aware activity
+ // [LiveActivityController createActivityWithActivityId:activityId completionHandler:^(void) {} ];
+ // 3. Create OneSignal-aware activity
+ [LiveActivityController createOneSignalAwareActivityWithActivityId:activityId];
}
} else {
NSLog(@"Must use iOS 13 or later for swift concurrency which is required for [LiveActivityController createActivityWithCompletionHandler...");
@@ -286,4 +290,21 @@ - (IBAction)dontRequireConsent:(id)sender {
[OneSignal setConsentRequired:false];
}
+- (IBAction)trackCustomEvents:(id)sender {
+ NSLog(@"Dev App: tracking some preset custom events");
+ [OneSignal.User trackEventWithName:@"simple event" properties:@{@"foobarbaz": @"foobarbaz"}];
+ NSMutableDictionary *dict = [NSMutableDictionary new];
+ dict[@"dict"] = @{@"abc" : @"def"};
+ dict[@"false"] = false;
+ dict[@"int"] = @99;
+ [OneSignal.User trackEventWithName:@"complex event" properties:dict];
+ [SwiftTest trackCustomEvents];
+}
+
+- (IBAction)trackNamedCustomEvent:(id)sender {
+ NSString *name = self.customEventsTextField.text;
+ NSLog(@"Dev App: Tracking custom event with name: %@", name);
+ [OneSignal.User trackEventWithName:name properties:nil];
+}
+
@end
diff --git a/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift b/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift
index 24e60b8e1..991048c14 100644
--- a/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift
+++ b/iOS_SDK/OneSignalDevApp/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift
@@ -52,9 +52,11 @@ import OneSignalLiveActivities
}
Spacer()
}
+ .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
+ // .widgetURL(URL(string: "myapp://product/12345"))
.activitySystemActionForegroundColor(.black)
.activityBackgroundTint(.white)
- } dynamicIsland: { _ in
+ } dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
@@ -75,7 +77,8 @@ import OneSignalLiveActivities
} minimal: {
Text("Min")
}
- .widgetURL(URL(string: "http://www.apple.com"))
+ .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
+ // .widgetURL(URL(string: "myapp://product/12345"))
.keylineTint(Color.red)
}
}
@@ -118,7 +121,8 @@ import OneSignalLiveActivities
.padding([.all], 20)
.activitySystemActionForegroundColor(.black)
.activityBackgroundTint(.white)
- } dynamicIsland: { _ in
+ .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
+ } dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
@@ -140,6 +144,7 @@ import OneSignalLiveActivities
Text("Min")
}
.keylineTint(Color.red)
+ .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
}
}
}
@@ -233,7 +238,8 @@ struct DefaultOneSignalLiveActivityWidget: Widget {
.padding([.all], 20)
.activitySystemActionForegroundColor(.black)
.activityBackgroundTint(.white)
- } dynamicIsland: { _ in
+ .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
+ } dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
@@ -255,6 +261,7 @@ struct DefaultOneSignalLiveActivityWidget: Widget {
Text("Min")
}
.keylineTint(Color.red)
+ .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
}
}
}
diff --git a/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata b/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata
index 9979cb733..f182a1cc5 100644
--- a/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata
+++ b/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata
@@ -7,4 +7,7 @@
+
+
diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj
index a6cf4783a..dd0004d02 100644
--- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj
+++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj
@@ -70,6 +70,7 @@
3C14E39F2AFAE39B006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E39E2AFAE39B006ED053 /* PrivacyInfo.xcprivacy */; };
3C14E3A12AFAE461006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E3A02AFAE461006ED053 /* PrivacyInfo.xcprivacy */; };
3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */; };
+ 3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */; };
3C24B0EC2BD09D7A0052E771 /* OneSignalCoreObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C24B0EB2BD09D7A0052E771 /* OneSignalCoreObjCTests.m */; };
3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C277D7D2BD76E0000857606 /* OSIdentityModelRepo.swift */; };
3C2C7DC8288F3C020020F9AE /* OSSubscriptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */; };
@@ -79,6 +80,10 @@
3C2FF9D02C5FCD760081293B /* OSUserJwtConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2FF9CF2C5FCD760081293B /* OSUserJwtConfig.swift */; };
3C3130E02CA383F800906665 /* OSUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3130DF2CA383F800906665 /* OSUser.swift */; };
3C3130E32CA3858500906665 /* OSPushSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3130E22CA3858500906665 /* OSPushSubscription.swift */; };
+ 3C30FE362F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */; };
+ 3C3D34E92E95EAA5006A2924 /* LiveActivityConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */; };
+ 3C3D8D782E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */; };
+ 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */; };
3C44673E296D099D0039A49E /* OneSignalMobileProvision.m in Sources */ = {isa = PBXBuildFile; fileRef = 912411FD1E73342200E41FD7 /* OneSignalMobileProvision.m */; };
3C44673F296D09CC0039A49E /* OneSignalMobileProvision.h in Headers */ = {isa = PBXBuildFile; fileRef = 912411FC1E73342200E41FD7 /* OneSignalMobileProvision.h */; settings = {ATTRIBUTES = (Public, ); }; };
3C448B9D2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */; };
@@ -95,13 +100,25 @@
3C5501432E09F3D900E77DF7 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5501422E09F3D900E77DF7 /* LoggingTests.swift */; };
3C5929E32CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5929E22CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift */; };
3C5929E52CAE523E0020D6FF /* MockUserJwtInvalidatedListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5929E42CAE523E0020D6FF /* MockUserJwtInvalidatedListener.swift */; };
+ 3C60BB9B2ECF860600C765F7 /* OneSignalInAppMessages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEBAAE282A4211D900BF2C1C /* OneSignalInAppMessages.framework */; };
+ 3C60BB9C2ECF860600C765F7 /* OneSignalInAppMessages.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DEBAAE282A4211D900BF2C1C /* OneSignalInAppMessages.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3C62999F2BEEA34800649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C62999E2BEEA34800649187 /* PrivacyInfo.xcprivacy */; };
3C6299A12BEEA38100649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A02BEEA38100649187 /* PrivacyInfo.xcprivacy */; };
3C6299A32BEEA3CC00649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A22BEEA3CC00649187 /* PrivacyInfo.xcprivacy */; };
3C6299A72BEEA41900649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A62BEEA40100649187 /* PrivacyInfo.xcprivacy */; };
3C6299A92BEEA46C00649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A82BEEA46C00649187 /* PrivacyInfo.xcprivacy */; };
3C6299AB2BEEA4C000649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299AA2BEEA4C000649187 /* PrivacyInfo.xcprivacy */; };
+ 3C64C3322F1066D700693230 /* LiveActivitiesManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C64C3312F1066D700693230 /* LiveActivitiesManagerTests.swift */; };
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */; };
+ 3C68EFE52D93195E00F0896B /* OSCustomEventsExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C68EFE42D93195E00F0896B /* OSCustomEventsExecutor.swift */; };
+ 3C68EFE72D931BA600F0896B /* OSRequestCustomEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C68EFE62D931BA600F0896B /* OSRequestCustomEvents.swift */; };
+ 3C7021E32ECF0821001768C6 /* OneSignalFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E2400381D4FFC31008BDE70 /* OneSignalFramework.framework */; };
+ 3C7021E42ECF0821001768C6 /* OneSignalFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3E2400381D4FFC31008BDE70 /* OneSignalFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 3C7021E92ECF0CF4001768C6 /* IAMIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C7021E82ECF0CF4001768C6 /* IAMIntegrationTests.swift */; };
+ 3C7022222ECF124B001768C6 /* OneSignalInAppMessagesMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C70221C2ECF124B001768C6 /* OneSignalInAppMessagesMocks.framework */; };
+ 3C7022232ECF124B001768C6 /* OneSignalInAppMessagesMocks.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C70221C2ECF124B001768C6 /* OneSignalInAppMessagesMocks.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 3C70222B2ECF126B001768C6 /* OneSignalInAppMessagesMocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C7022292ECF126B001768C6 /* OneSignalInAppMessagesMocks.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 3C70222D2ECF12A5001768C6 /* IAMTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C70222C2ECF12A5001768C6 /* IAMTestHelpers.swift */; };
3C70FA672D0B68A100031066 /* OneSignalClientError.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C70FA652D0B68A100031066 /* OneSignalClientError.h */; settings = {ATTRIBUTES = (Public, ); }; };
3C70FA682D0B68A100031066 /* OneSignalClientError.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C70FA662D0B68A100031066 /* OneSignalClientError.m */; };
3C789DBD293C2206004CF83D /* OSFocusInfluenceParam.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A600B432453790700514A53 /* OSFocusInfluenceParam.m */; };
@@ -146,6 +163,11 @@
3CA6CE0A28E4F19B00CA0585 /* OSUserRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA6CE0928E4F19B00CA0585 /* OSUserRequest.swift */; };
3CA8B8822BEC2FCB0010ADA1 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C7A39D42B7C18EE0082665E /* XCTest.framework */; };
3CA8B8832BEC2FCB0010ADA1 /* XCTest.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C7A39D42B7C18EE0082665E /* XCTest.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 3CAA4BB72F0BAFBA00A16682 /* TriggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */; };
+ 3CB331682F281679000E1801 /* CustomEventsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB331672F281679000E1801 /* CustomEventsIntegrationTests.swift */; };
+ 3CB3316A2F281692000E1801 /* OSCustomEventsExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB331692F281692000E1801 /* OSCustomEventsExecutorTests.swift */; };
+ 3CB35FCB2F0FA20B000E6E0F /* OSMessagingControllerUserStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CB35FCA2F0FA20B000E6E0F /* OSMessagingControllerUserStateTests.swift */; };
+ 3CBB6C262ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBB6C252ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift */; };
3CC063942B6D6B6B002BB07F /* OneSignalCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063932B6D6B6B002BB07F /* OneSignalCore.m */; };
3CC063A22B6D7A8E002BB07F /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; };
3CC063A72B6D7A8E002BB07F /* OneSignalCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063A62B6D7A8E002BB07F /* OneSignalCoreTests.swift */; };
@@ -198,6 +220,18 @@
3CF8629E28A183F900776CA4 /* OSIdentityModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */; };
3CF862A028A1964F00776CA4 /* OSPropertiesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */; };
3CF862A228A197D200776CA4 /* OSPropertiesModelStoreListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */; };
+ 3CFA8F4F2E9087DB00201FE5 /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F492E9087DB00201FE5 /* AnyCodable.swift */; };
+ 3CFA8F502E9087DB00201FE5 /* OSLiveActivitiesExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F412E9087DB00201FE5 /* OSLiveActivitiesExecutor.swift */; };
+ 3CFA8F512E9087DB00201FE5 /* DefaultLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F4A2E9087DB00201FE5 /* DefaultLiveActivityAttributes.swift */; };
+ 3CFA8F522E9087DB00201FE5 /* OSRequestSetStartToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F462E9087DB00201FE5 /* OSRequestSetStartToken.swift */; };
+ 3CFA8F532E9087DB00201FE5 /* OSRequestRemoveStartToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F442E9087DB00201FE5 /* OSRequestRemoveStartToken.swift */; };
+ 3CFA8F542E9087DB00201FE5 /* OSLiveActivitiesExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F4D2E9087DB00201FE5 /* OSLiveActivitiesExtension.swift */; };
+ 3CFA8F552E9087DB00201FE5 /* OneSignalLiveActivitiesManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F4B2E9087DB00201FE5 /* OneSignalLiveActivitiesManagerImpl.swift */; };
+ 3CFA8F562E9087DB00201FE5 /* OSRequestSetUpdateToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F472E9087DB00201FE5 /* OSRequestSetUpdateToken.swift */; };
+ 3CFA8F572E9087DB00201FE5 /* OSRequestRemoveUpdateToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F452E9087DB00201FE5 /* OSRequestRemoveUpdateToken.swift */; };
+ 3CFA8F582E9087DB00201FE5 /* OSLiveActivityRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F432E9087DB00201FE5 /* OSLiveActivityRequest.swift */; };
+ 3CFA8F592E9087DB00201FE5 /* OneSignalLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F4C2E9087DB00201FE5 /* OneSignalLiveActivityAttributes.swift */; };
+ 3CFA8F5B2E9091A200201FE5 /* OSRequestLiveActivityReceiveReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CFA8F5A2E9091A200201FE5 /* OSRequestLiveActivityReceiveReceipts.swift */; };
3E464ED71D88ED1F00DCF7E9 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37E6B2BA19D9CAF300D0C601 /* UIKit.framework */; };
3E66F5821D90A2C600E45A01 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3E08E2701D49A5C8002176DE /* SystemConfiguration.framework */; };
4529DED21FA81EA800CEAB1D /* NSObjectOverrider.m in Sources */ = {isa = PBXBuildFile; fileRef = 4529DED11FA81EA800CEAB1D /* NSObjectOverrider.m */; };
@@ -215,7 +249,6 @@
4710EA562B8FD08F00435356 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; };
4710EA572B8FD08F00435356 /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4710EA5A2B8FD18800435356 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; };
- 47278E452BD7E62B00562820 /* DefaultLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47278E442BD7E62B00562820 /* DefaultLiveActivityAttributes.swift */; };
47278E472BD92B4B00562820 /* DefaultLiveActivityAttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47278E462BD92B4B00562820 /* DefaultLiveActivityAttributesTests.swift */; };
4735424D2B8F93340016DB4C /* OSLiveActivitiesExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4735424C2B8F93340016DB4C /* OSLiveActivitiesExecutorTests.swift */; };
473542552B8F93760016DB4C /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; };
@@ -235,19 +268,9 @@
475F47252B8E398E00EC05B3 /* OneSignalLiveActivities.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 475F471E2B8E398D00EC05B3 /* OneSignalLiveActivities.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
475F472A2B8E399F00EC05B3 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; };
475F472E2B8E399F00EC05B3 /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; };
- 475F47362B8E39DD00EC05B3 /* OSLiveActivitiesExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F47352B8E39DD00EC05B3 /* OSLiveActivitiesExecutor.swift */; };
- 475F473A2B8E39F300EC05B3 /* OneSignalLiveActivitiesManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F47372B8E39F300EC05B3 /* OneSignalLiveActivitiesManagerImpl.swift */; };
- 475F473B2B8E39F300EC05B3 /* OneSignalLiveActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F47382B8E39F300EC05B3 /* OneSignalLiveActivityAttributes.swift */; };
- 475F473C2B8E39F300EC05B3 /* OSLiveActivitiesExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F47392B8E39F300EC05B3 /* OSLiveActivitiesExtension.swift */; };
- 475F47422B8E3A0A00EC05B3 /* OSRequestRemoveUpdateToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F473D2B8E3A0900EC05B3 /* OSRequestRemoveUpdateToken.swift */; };
- 475F47432B8E3A0A00EC05B3 /* OSRequestSetStartToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F473E2B8E3A0900EC05B3 /* OSRequestSetStartToken.swift */; };
- 475F47442B8E3A0A00EC05B3 /* OSRequestSetUpdateToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F473F2B8E3A0A00EC05B3 /* OSRequestSetUpdateToken.swift */; };
- 475F47452B8E3A0A00EC05B3 /* OSLiveActivityRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F47402B8E3A0A00EC05B3 /* OSLiveActivityRequest.swift */; };
- 475F47462B8E3A0A00EC05B3 /* OSRequestRemoveStartToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F47412B8E3A0A00EC05B3 /* OSRequestRemoveStartToken.swift */; };
475F474A2B8E3B4600EC05B3 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 475F471E2B8E398D00EC05B3 /* OneSignalLiveActivities.framework */; platformFilter = ios; };
475F474F2B8E3B5400EC05B3 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 475F471E2B8E398D00EC05B3 /* OneSignalLiveActivities.framework */; };
475F47502B8E3B5400EC05B3 /* OneSignalLiveActivities.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 475F471E2B8E398D00EC05B3 /* OneSignalLiveActivities.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 47A885CD2BB317B300ED91FA /* AnyCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A885CC2BB317B300ED91FA /* AnyCodable.swift */; };
5B053FBC2CAE07EB002F30C4 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; };
5B053FC32CAE0843002F30C4 /* OSConsistencyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC1DE672C90C23E00CA8807 /* OSConsistencyManagerTests.swift */; };
5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B58E4F6237CE7B4009401E0 /* UIDeviceOverrider.m */; };
@@ -482,7 +505,7 @@
DE7D18C627038249002D3A5D /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D188027037F43002D3A5D /* OneSignalOutcomes.framework */; };
DE7D18CD270385D0002D3A5D /* OSOutcomesRequests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7D18CB270385D0002D3A5D /* OSOutcomesRequests.m */; };
DE7D18CF270385E0002D3A5D /* OSOutcomesRequests.h in Headers */ = {isa = PBXBuildFile; fileRef = DE7D18CE270385E0002D3A5D /* OSOutcomesRequests.h */; };
- DE7D18D1270389E1002D3A5D /* OSMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AE28B8725B8ADF400529100 /* OSMacros.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ DE7D18D1270389E1002D3A5D /* OSMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AE28B8725B8ADF400529100 /* OSMacros.h */; settings = {ATTRIBUTES = (Private, ); }; };
DE7D18D22703ADE0002D3A5D /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D188027037F43002D3A5D /* OneSignalOutcomes.framework */; };
DE7D18D62703B103002D3A5D /* OSInAppMessageOutcome.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A880F2A23FB45FB0081F5E8 /* OSInAppMessageOutcome.m */; };
DE7D18D72703B111002D3A5D /* OSInAppMessageOutcome.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A880F2923FB45CE0081F5E8 /* OSInAppMessageOutcome.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -655,6 +678,27 @@
remoteGlobalIDString = 3C115160289A259500565C41;
remoteInfo = OneSignalOSCore;
};
+ 3C60BB9D2ECF860600C765F7 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 37747F8B19147D6400558FAD /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DEBAAE272A4211D900BF2C1C;
+ remoteInfo = OneSignalInAppMessages;
+ };
+ 3C7021E52ECF0821001768C6 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 37747F8B19147D6400558FAD /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 3E2400371D4FFC31008BDE70;
+ remoteInfo = OneSignalFramework;
+ };
+ 3C7022202ECF124B001768C6 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 37747F8B19147D6400558FAD /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 3C70221B2ECF124B001768C6;
+ remoteInfo = OneSignalInAppMessagesMocks;
+ };
3C7A39C32B7BED900082665E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 37747F8B19147D6400558FAD /* Project object */;
@@ -1116,6 +1160,17 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 3C60BB9F2ECF860600C765F7 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 3C60BB9C2ECF860600C765F7 /* OneSignalInAppMessages.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
3C7A39C52B7BED910082665E /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -1218,7 +1273,9 @@
DEA4B4662888C59E00E9FE12 /* OneSignalExtension.framework in Embed Frameworks */,
DEBAAE2F2A4211DA00BF2C1C /* OneSignalInAppMessages.framework in Embed Frameworks */,
475F47252B8E398E00EC05B3 /* OneSignalLiveActivities.framework in Embed Frameworks */,
+ 3C7022232ECF124B001768C6 /* OneSignalInAppMessagesMocks.framework in Embed Frameworks */,
3C8544BD2C5AEFF700F542A9 /* OneSignalOSCoreMocks.framework in Embed Frameworks */,
+ 3C7021E42ECF0821001768C6 /* OneSignalFramework.framework in Embed Frameworks */,
DEA4B4632888C4DC00E9FE12 /* OneSignalOutcomes.framework in Embed Frameworks */,
3CEE934B2B7C73B6008440BD /* OneSignalUserMocks.framework in Embed Frameworks */,
DEA4B45D2888C1D000E9FE12 /* OneSignalCore.framework in Embed Frameworks */,
@@ -1239,8 +1296,8 @@
03866CBF2378A67B0009C1D8 /* RestClientAsserts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RestClientAsserts.h; sourceTree = ""; };
03866CC02378A67B0009C1D8 /* RestClientAsserts.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RestClientAsserts.m; sourceTree = ""; };
03CCCC7D2835D8CC004BF794 /* OneSignalUNUserNotificationCenterSwizzlingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OneSignalUNUserNotificationCenterSwizzlingTest.m; sourceTree = ""; };
- 03CCCC812835D90F004BF794 /* OneSignalUNUserNotificationCenterHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OneSignalUNUserNotificationCenterHelper.m; path = UNNotificationCenter/OneSignalUNUserNotificationCenterHelper.m; sourceTree = ""; };
- 03CCCC822835D90F004BF794 /* OneSignalUNUserNotificationCenterHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OneSignalUNUserNotificationCenterHelper.h; path = UNNotificationCenter/OneSignalUNUserNotificationCenterHelper.h; sourceTree = ""; };
+ 03CCCC812835D90F004BF794 /* OneSignalUNUserNotificationCenterHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OneSignalUNUserNotificationCenterHelper.m; sourceTree = ""; };
+ 03CCCC822835D90F004BF794 /* OneSignalUNUserNotificationCenterHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OneSignalUNUserNotificationCenterHelper.h; sourceTree = ""; };
03CCCC842835F291004BF794 /* UIApplicationDelegateSwizzlingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIApplicationDelegateSwizzlingTests.m; sourceTree = ""; };
03E56DD128405F4A006AA1DA /* OneSignalAppDelegateOverrider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalAppDelegateOverrider.h; sourceTree = ""; };
03E56DD228405F4A006AA1DA /* OneSignalAppDelegateOverrider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalAppDelegateOverrider.m; sourceTree = ""; };
@@ -1264,6 +1321,7 @@
3C11518C289AF5E800565C41 /* OSModelChangedHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSModelChangedHandler.swift; sourceTree = ""; };
3C14E39E2AFAE39B006ED053 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
3C14E3A02AFAE461006ED053 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
+ 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestLiveActivityClicked.swift; sourceTree = ""; };
3C24B0EA2BD09D790052E771 /* OneSignalCoreTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OneSignalCoreTests-Bridging-Header.h"; sourceTree = ""; };
3C24B0EB2BD09D7A0052E771 /* OneSignalCoreObjCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalCoreObjCTests.m; sourceTree = ""; };
3C277D7D2BD76E0000857606 /* OSIdentityModelRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModelRepo.swift; sourceTree = ""; };
@@ -1275,6 +1333,10 @@
3C2FF9CF2C5FCD760081293B /* OSUserJwtConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserJwtConfig.swift; sourceTree = ""; };
3C3130DF2CA383F800906665 /* OSUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUser.swift; sourceTree = ""; };
3C3130E22CA3858500906665 /* OSPushSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPushSubscription.swift; sourceTree = ""; };
+ 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarlyTriggerTrackingTests.swift; sourceTree = ""; };
+ 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityConstants.swift; sourceTree = ""; };
+ 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLiveActivityViewExtensions.swift; sourceTree = ""; };
+ 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionEndOutcomesRequestTests.swift; sourceTree = ""; };
3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSBackgroundTaskHandlerImpl.h; sourceTree = ""; };
3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSBackgroundTaskHandlerImpl.m; sourceTree = ""; };
3C448BA12936B474002F96BC /* OSBackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSBackgroundTaskManager.swift; sourceTree = ""; };
@@ -1293,7 +1355,15 @@
3C6299A62BEEA40100649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
3C6299A82BEEA46C00649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
3C6299AA2BEEA4C000649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
+ 3C64C3312F1066D700693230 /* LiveActivitiesManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitiesManagerTests.swift; sourceTree = ""; };
3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchUserIntegrationTests.swift; sourceTree = ""; };
+ 3C68EFE42D93195E00F0896B /* OSCustomEventsExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCustomEventsExecutor.swift; sourceTree = ""; };
+ 3C68EFE62D931BA600F0896B /* OSRequestCustomEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestCustomEvents.swift; sourceTree = ""; };
+ 3C7021E72ECF0CF3001768C6 /* OneSignalInAppMessagesTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OneSignalInAppMessagesTests-Bridging-Header.h"; sourceTree = ""; };
+ 3C7021E82ECF0CF4001768C6 /* IAMIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAMIntegrationTests.swift; sourceTree = ""; };
+ 3C70221C2ECF124B001768C6 /* OneSignalInAppMessagesMocks.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalInAppMessagesMocks.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3C7022292ECF126B001768C6 /* OneSignalInAppMessagesMocks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalInAppMessagesMocks.h; sourceTree = ""; };
+ 3C70222C2ECF12A5001768C6 /* IAMTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAMTestHelpers.swift; sourceTree = ""; };
3C70FA652D0B68A100031066 /* OneSignalClientError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalClientError.h; sourceTree = ""; };
3C70FA662D0B68A100031066 /* OneSignalClientError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalClientError.m; sourceTree = ""; };
3C7A39D42B7C18EE0082665E /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
@@ -1321,6 +1391,11 @@
3C9AD6D02B228B9200BC1540 /* OSRequestRemoveAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestRemoveAlias.swift; sourceTree = ""; };
3C9AD6D22B228BB000BC1540 /* OSRequestUpdateProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestUpdateProperties.swift; sourceTree = ""; };
3CA6CE0928E4F19B00CA0585 /* OSUserRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserRequest.swift; sourceTree = ""; };
+ 3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerTests.swift; sourceTree = ""; };
+ 3CB331672F281679000E1801 /* CustomEventsIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEventsIntegrationTests.swift; sourceTree = ""; };
+ 3CB331692F281692000E1801 /* OSCustomEventsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCustomEventsExecutorTests.swift; sourceTree = ""; };
+ 3CB35FCA2F0FA20B000E6E0F /* OSMessagingControllerUserStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMessagingControllerUserStateTests.swift; sourceTree = ""; };
+ 3CBB6C252ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsistencyManagerTestHelpers.swift; sourceTree = ""; };
3CC063932B6D6B6B002BB07F /* OneSignalCore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalCore.m; sourceTree = ""; };
3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalCoreMocks.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3CC0639C2B6D7A8D002BB07F /* OneSignalCoreMocks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalCoreMocks.h; sourceTree = ""; };
@@ -1360,6 +1435,18 @@
3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModel.swift; sourceTree = ""; };
3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModel.swift; sourceTree = ""; };
3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModelStoreListener.swift; sourceTree = ""; };
+ 3CFA8F412E9087DB00201FE5 /* OSLiveActivitiesExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLiveActivitiesExecutor.swift; sourceTree = ""; };
+ 3CFA8F432E9087DB00201FE5 /* OSLiveActivityRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLiveActivityRequest.swift; sourceTree = ""; };
+ 3CFA8F442E9087DB00201FE5 /* OSRequestRemoveStartToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestRemoveStartToken.swift; sourceTree = ""; };
+ 3CFA8F452E9087DB00201FE5 /* OSRequestRemoveUpdateToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestRemoveUpdateToken.swift; sourceTree = ""; };
+ 3CFA8F462E9087DB00201FE5 /* OSRequestSetStartToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestSetStartToken.swift; sourceTree = ""; };
+ 3CFA8F472E9087DB00201FE5 /* OSRequestSetUpdateToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestSetUpdateToken.swift; sourceTree = ""; };
+ 3CFA8F492E9087DB00201FE5 /* AnyCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCodable.swift; sourceTree = ""; };
+ 3CFA8F4A2E9087DB00201FE5 /* DefaultLiveActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultLiveActivityAttributes.swift; sourceTree = ""; };
+ 3CFA8F4B2E9087DB00201FE5 /* OneSignalLiveActivitiesManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalLiveActivitiesManagerImpl.swift; sourceTree = ""; };
+ 3CFA8F4C2E9087DB00201FE5 /* OneSignalLiveActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalLiveActivityAttributes.swift; sourceTree = ""; };
+ 3CFA8F4D2E9087DB00201FE5 /* OSLiveActivitiesExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLiveActivitiesExtension.swift; sourceTree = ""; };
+ 3CFA8F5A2E9091A200201FE5 /* OSRequestLiveActivityReceiveReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestLiveActivityReceiveReceipts.swift; sourceTree = ""; };
3E08E2701D49A5C8002176DE /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
3E2400381D4FFC31008BDE70 /* OneSignalFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3E24003B1D4FFC31008BDE70 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
@@ -1396,7 +1483,6 @@
454F94F61FAD2EC300D74CCF /* OSNotification+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OSNotification+Internal.h"; sourceTree = ""; };
4710EA522B8FCFB200435356 /* OSDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDispatchQueue.swift; sourceTree = ""; };
4710EA542B8FD04400435356 /* MockOSDispatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOSDispatchQueue.swift; sourceTree = ""; };
- 47278E442BD7E62B00562820 /* DefaultLiveActivityAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DefaultLiveActivityAttributes.swift; path = Source/DefaultLiveActivityAttributes.swift; sourceTree = ""; };
47278E462BD92B4B00562820 /* DefaultLiveActivityAttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultLiveActivityAttributesTests.swift; sourceTree = ""; };
4735424A2B8F93330016DB4C /* OneSignalLiveActivitiesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OneSignalLiveActivitiesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4735424C2B8F93340016DB4C /* OSLiveActivitiesExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLiveActivitiesExecutorTests.swift; sourceTree = ""; };
@@ -1404,17 +1490,7 @@
4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LiveActivitiesObjcTests.m; sourceTree = ""; };
475F471E2B8E398D00EC05B3 /* OneSignalLiveActivities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalLiveActivities.framework; sourceTree = BUILT_PRODUCTS_DIR; };
475F47202B8E398E00EC05B3 /* OneSignalLiveActivities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalLiveActivities.h; sourceTree = ""; };
- 475F47352B8E39DD00EC05B3 /* OSLiveActivitiesExecutor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OSLiveActivitiesExecutor.swift; path = Source/Executors/OSLiveActivitiesExecutor.swift; sourceTree = ""; };
- 475F47372B8E39F300EC05B3 /* OneSignalLiveActivitiesManagerImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OneSignalLiveActivitiesManagerImpl.swift; path = Source/OneSignalLiveActivitiesManagerImpl.swift; sourceTree = ""; };
- 475F47382B8E39F300EC05B3 /* OneSignalLiveActivityAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OneSignalLiveActivityAttributes.swift; path = Source/OneSignalLiveActivityAttributes.swift; sourceTree = ""; };
- 475F47392B8E39F300EC05B3 /* OSLiveActivitiesExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OSLiveActivitiesExtension.swift; path = Source/OSLiveActivitiesExtension.swift; sourceTree = ""; };
- 475F473D2B8E3A0900EC05B3 /* OSRequestRemoveUpdateToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OSRequestRemoveUpdateToken.swift; path = Source/Requests/OSRequestRemoveUpdateToken.swift; sourceTree = ""; };
- 475F473E2B8E3A0900EC05B3 /* OSRequestSetStartToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OSRequestSetStartToken.swift; path = Source/Requests/OSRequestSetStartToken.swift; sourceTree = ""; };
- 475F473F2B8E3A0A00EC05B3 /* OSRequestSetUpdateToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OSRequestSetUpdateToken.swift; path = Source/Requests/OSRequestSetUpdateToken.swift; sourceTree = ""; };
- 475F47402B8E3A0A00EC05B3 /* OSLiveActivityRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OSLiveActivityRequest.swift; path = Source/Requests/OSLiveActivityRequest.swift; sourceTree = ""; };
- 475F47412B8E3A0A00EC05B3 /* OSRequestRemoveStartToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OSRequestRemoveStartToken.swift; path = Source/Requests/OSRequestRemoveStartToken.swift; sourceTree = ""; };
- 475F47482B8E3A4400EC05B3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = OneSignalLiveActivitiesFramework/Info.plist; sourceTree = ""; };
- 47A885CC2BB317B300ED91FA /* AnyCodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AnyCodable.swift; path = Source/AnyCodable.swift; sourceTree = ""; };
+ 475F47482B8E3A4400EC05B3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
5B053FB82CAE07EB002F30C4 /* OneSignalOSCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OneSignalOSCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
5B58E4F3237CE7B3009401E0 /* UIDeviceOverrider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIDeviceOverrider.h; sourceTree = ""; };
5B58E4F6237CE7B4009401E0 /* UIDeviceOverrider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIDeviceOverrider.m; sourceTree = ""; };
@@ -1762,6 +1838,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 3C7022192ECF124B001768C6 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3C60BB9B2ECF860600C765F7 /* OneSignalInAppMessages.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
3C8544B32C5AEFF600F542A9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -1963,6 +2047,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 3C7021E32ECF0821001768C6 /* OneSignalFramework.framework in Frameworks */,
3C11518E289AF83600565C41 /* OneSignalOSCore.framework in Frameworks */,
DE3784852888D00300453A8E /* OneSignalUser.framework in Frameworks */,
DEBAAE2E2A4211DA00BF2C1C /* OneSignalInAppMessages.framework in Frameworks */,
@@ -1976,6 +2061,7 @@
3CEE934A2B7C73B6008440BD /* OneSignalUserMocks.framework in Frameworks */,
DEF5CD52253934410003E9CC /* CoreFoundation.framework in Frameworks */,
DEF5CD502539343C0003E9CC /* Foundation.framework in Frameworks */,
+ 3C7022222ECF124B001768C6 /* OneSignalInAppMessagesMocks.framework in Frameworks */,
DEF5CD4F253934350003E9CC /* UIKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2013,7 +2099,7 @@
03CCCC822835D90F004BF794 /* OneSignalUNUserNotificationCenterHelper.h */,
03CCCC812835D90F004BF794 /* OneSignalUNUserNotificationCenterHelper.m */,
);
- name = UNNotificationCenter;
+ path = UNNotificationCenter;
sourceTree = "";
};
37747F8A19147D6400558FAD = {
@@ -2046,6 +2132,7 @@
3CC0639B2B6D7A8D002BB07F /* OneSignalCoreMocks */,
3C8544B72C5AEFF700F542A9 /* OneSignalOSCoreMocks */,
3CC063DE2B6D7F2A002BB07F /* OneSignalUserMocks */,
+ 3C70222A2ECF126B001768C6 /* OneSignalInAppMessagesMocks */,
3CC063A52B6D7A8E002BB07F /* OneSignalCoreTests */,
3CC063EC2B6D7FE8002BB07F /* OneSignalUserTests */,
3C01518F2C2E298F0079E076 /* OneSignalInAppMessagesTests */,
@@ -2080,6 +2167,7 @@
3C01518E2C2E298E0079E076 /* OneSignalInAppMessagesTests.xctest */,
3C8544B62C5AEFF600F542A9 /* OneSignalOSCoreMocks.framework */,
5B053FB82CAE07EB002F30C4 /* OneSignalOSCoreTests.xctest */,
+ 3C70221C2ECF124B001768C6 /* OneSignalInAppMessagesMocks.framework */,
);
name = Products;
sourceTree = "";
@@ -2116,6 +2204,11 @@
isa = PBXGroup;
children = (
3C01519B2C2E29F90079E076 /* IAMRequestTests.m */,
+ 3C7021E82ECF0CF4001768C6 /* IAMIntegrationTests.swift */,
+ 3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */,
+ 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */,
+ 3CB35FCA2F0FA20B000E6E0F /* OSMessagingControllerUserStateTests.swift */,
+ 3C7021E72ECF0CF3001768C6 /* OneSignalInAppMessagesTests-Bridging-Header.h */,
);
path = OneSignalInAppMessagesTests;
sourceTree = "";
@@ -2188,11 +2281,21 @@
path = Public;
sourceTree = "";
};
+ 3C70222A2ECF126B001768C6 /* OneSignalInAppMessagesMocks */ = {
+ isa = PBXGroup;
+ children = (
+ 3C7022292ECF126B001768C6 /* OneSignalInAppMessagesMocks.h */,
+ 3C70222C2ECF12A5001768C6 /* IAMTestHelpers.swift */,
+ );
+ path = OneSignalInAppMessagesMocks;
+ sourceTree = "";
+ };
3C8544B72C5AEFF700F542A9 /* OneSignalOSCoreMocks */ = {
isa = PBXGroup;
children = (
3C8544B82C5AEFF700F542A9 /* OneSignalOSCoreMocks.h */,
3C8544C22C5AF18B00F542A9 /* OSCoreMocks.swift */,
+ 3CBB6C252ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift */,
3CF11E3F2C6E6DE2002856F5 /* MockNewRecordsState.swift */,
);
path = OneSignalOSCoreMocks;
@@ -2211,6 +2314,7 @@
isa = PBXGroup;
children = (
3C8E6E0028AC0BA10031E48A /* OSIdentityOperationExecutor.swift */,
+ 3C68EFE42D93195E00F0896B /* OSCustomEventsExecutor.swift */,
3C8E6DFE28AB09AE0031E48A /* OSPropertyOperationExecutor.swift */,
3CE795FA28DBDCE700736BD4 /* OSSubscriptionOperationExecutor.swift */,
3C9AD6BB2B2285FB00BC1540 /* OSUserExecutor.swift */,
@@ -2233,6 +2337,7 @@
3C9AD6C62B228A9800BC1540 /* OSRequestTransferSubscription.swift */,
3C9AD6C02B22886600BC1540 /* OSRequestUpdateSubscription.swift */,
3C9AD6C42B228A7300BC1540 /* OSRequestDeleteSubscription.swift */,
+ 3C68EFE62D931BA600F0896B /* OSRequestCustomEvents.swift */,
);
path = Requests;
sourceTree = "";
@@ -2280,6 +2385,7 @@
3CF11E3E2C6D61AC002856F5 /* Executors */,
3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */,
3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */,
+ 3CB331672F281679000E1801 /* CustomEventsIntegrationTests.swift */,
3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */,
3CDE664B2BFC2A56006DA114 /* OneSignalUserObjcTests.m */,
);
@@ -2302,10 +2408,49 @@
DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */,
DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */,
DE3568F12C8911EA00AF447C /* IdentityExecutorTests.swift */,
+ 3CB331692F281692000E1801 /* OSCustomEventsExecutorTests.swift */,
);
path = Executors;
sourceTree = "";
};
+ 3CFA8F422E9087DB00201FE5 /* Executors */ = {
+ isa = PBXGroup;
+ children = (
+ 3CFA8F412E9087DB00201FE5 /* OSLiveActivitiesExecutor.swift */,
+ );
+ path = Executors;
+ sourceTree = "";
+ };
+ 3CFA8F482E9087DB00201FE5 /* Requests */ = {
+ isa = PBXGroup;
+ children = (
+ 3CFA8F432E9087DB00201FE5 /* OSLiveActivityRequest.swift */,
+ 3CFA8F462E9087DB00201FE5 /* OSRequestSetStartToken.swift */,
+ 3CFA8F442E9087DB00201FE5 /* OSRequestRemoveStartToken.swift */,
+ 3CFA8F472E9087DB00201FE5 /* OSRequestSetUpdateToken.swift */,
+ 3CFA8F452E9087DB00201FE5 /* OSRequestRemoveUpdateToken.swift */,
+ 3CFA8F5A2E9091A200201FE5 /* OSRequestLiveActivityReceiveReceipts.swift */,
+ 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */,
+ );
+ path = Requests;
+ sourceTree = "";
+ };
+ 3CFA8F4E2E9087DB00201FE5 /* Source */ = {
+ isa = PBXGroup;
+ children = (
+ 3CFA8F422E9087DB00201FE5 /* Executors */,
+ 3CFA8F482E9087DB00201FE5 /* Requests */,
+ 3CFA8F4B2E9087DB00201FE5 /* OneSignalLiveActivitiesManagerImpl.swift */,
+ 3CFA8F4C2E9087DB00201FE5 /* OneSignalLiveActivityAttributes.swift */,
+ 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */,
+ 3CFA8F4D2E9087DB00201FE5 /* OSLiveActivitiesExtension.swift */,
+ 3CFA8F492E9087DB00201FE5 /* AnyCodable.swift */,
+ 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */,
+ 3CFA8F4A2E9087DB00201FE5 /* DefaultLiveActivityAttributes.swift */,
+ );
+ path = Source;
+ sourceTree = "";
+ };
3E2400391D4FFC31008BDE70 /* OneSignalFramework */ = {
isa = PBXGroup;
children = (
@@ -2378,6 +2523,7 @@
isa = PBXGroup;
children = (
4735424C2B8F93340016DB4C /* OSLiveActivitiesExecutorTests.swift */,
+ 3C64C3312F1066D700693230 /* LiveActivitiesManagerTests.swift */,
47278E462BD92B4B00562820 /* DefaultLiveActivityAttributesTests.swift */,
);
path = OneSignalLiveActivitiesTests;
@@ -2386,45 +2532,19 @@
475F471F2B8E398E00EC05B3 /* OneSignalLiveActivities */ = {
isa = PBXGroup;
children = (
- 475F47342B8E39B700EC05B3 /* Executors */,
- 475F47332B8E39B100EC05B3 /* Requests */,
- 475F47372B8E39F300EC05B3 /* OneSignalLiveActivitiesManagerImpl.swift */,
- 475F47382B8E39F300EC05B3 /* OneSignalLiveActivityAttributes.swift */,
- 475F47392B8E39F300EC05B3 /* OSLiveActivitiesExtension.swift */,
+ 3CFA8F4E2E9087DB00201FE5 /* Source */,
475F47202B8E398E00EC05B3 /* OneSignalLiveActivities.h */,
- 47A885CC2BB317B300ED91FA /* AnyCodable.swift */,
- 47278E442BD7E62B00562820 /* DefaultLiveActivityAttributes.swift */,
3C6299A22BEEA3CC00649187 /* PrivacyInfo.xcprivacy */,
);
path = OneSignalLiveActivities;
sourceTree = "";
};
- 475F47332B8E39B100EC05B3 /* Requests */ = {
- isa = PBXGroup;
- children = (
- 475F47402B8E3A0A00EC05B3 /* OSLiveActivityRequest.swift */,
- 475F473E2B8E3A0900EC05B3 /* OSRequestSetStartToken.swift */,
- 475F47412B8E3A0A00EC05B3 /* OSRequestRemoveStartToken.swift */,
- 475F473F2B8E3A0A00EC05B3 /* OSRequestSetUpdateToken.swift */,
- 475F473D2B8E3A0900EC05B3 /* OSRequestRemoveUpdateToken.swift */,
- );
- name = Requests;
- sourceTree = "";
- };
- 475F47342B8E39B700EC05B3 /* Executors */ = {
- isa = PBXGroup;
- children = (
- 475F47352B8E39DD00EC05B3 /* OSLiveActivitiesExecutor.swift */,
- );
- name = Executors;
- sourceTree = "";
- };
475F47472B8E3A1C00EC05B3 /* OneSignalLiveActivitiesFramework */ = {
isa = PBXGroup;
children = (
475F47482B8E3A4400EC05B3 /* Info.plist */,
);
- name = OneSignalLiveActivitiesFramework;
+ path = OneSignalLiveActivitiesFramework;
sourceTree = "";
};
5B053FB92CAE07EB002F30C4 /* OneSignalOSCoreTests */ = {
@@ -2535,6 +2655,7 @@
3C2C7DC2288E007E0020F9AE /* UnitTests-Bridging-Header.h */,
4746E2A62B86B64100D6324C /* LiveActivitiesSwiftTests.swift */,
4746E2AA2B8775C400D6324C /* LiveActivitiesObjcTests.m */,
+ 3C4319082F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift */,
);
path = UnitTests;
sourceTree = "";
@@ -2581,8 +2702,6 @@
CA1A6E6820DC2E31001C41B9 /* OneSignalDialogController.m */,
CA1A6E6D20DC2E73001C41B9 /* OneSignalDialogRequest.h */,
CA1A6E6E20DC2E73001C41B9 /* OneSignalDialogRequest.m */,
- DE20425C24E21C1500350E4F /* UIApplication+OneSignal.h */,
- DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */,
);
name = Categories;
sourceTree = "";
@@ -2971,6 +3090,8 @@
DEBAAE4C2A42157B00BF2C1C /* UI */,
DEBAAE2A2A4211DA00BF2C1C /* OneSignalInAppMessages.h */,
DEBAAE982A42179A00BF2C1C /* OneSignalInAppMessages.m */,
+ DE20425C24E21C1500350E4F /* UIApplication+OneSignal.h */,
+ DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */,
DEBAAE962A42178800BF2C1C /* OSInAppMessagingDefines.h */,
3C14E39E2AFAE39B006ED053 /* PrivacyInfo.xcprivacy */,
);
@@ -3132,6 +3253,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 3C7022172ECF124B001768C6 /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3C70222B2ECF126B001768C6 /* OneSignalInAppMessagesMocks.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
3C8544B12C5AEFF600F542A9 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
@@ -3396,6 +3525,28 @@
productReference = 3C115161289A259500565C41 /* OneSignalOSCore.framework */;
productType = "com.apple.product-type.framework";
};
+ 3C70221B2ECF124B001768C6 /* OneSignalInAppMessagesMocks */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 3C7022282ECF124B001768C6 /* Build configuration list for PBXNativeTarget "OneSignalInAppMessagesMocks" */;
+ buildPhases = (
+ 3C7022172ECF124B001768C6 /* Headers */,
+ 3C7022182ECF124B001768C6 /* Sources */,
+ 3C7022192ECF124B001768C6 /* Frameworks */,
+ 3C70221A2ECF124B001768C6 /* Resources */,
+ 3C60BB9F2ECF860600C765F7 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 3C60BB9E2ECF860600C765F7 /* PBXTargetDependency */,
+ );
+ name = OneSignalInAppMessagesMocks;
+ packageProductDependencies = (
+ );
+ productName = OneSignalInAppMessagesMocks;
+ productReference = 3C70221C2ECF124B001768C6 /* OneSignalInAppMessagesMocks.framework */;
+ productType = "com.apple.product-type.framework";
+ };
3C8544B52C5AEFF600F542A9 /* OneSignalOSCoreMocks */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3C8544C12C5AEFF800F542A9 /* Build configuration list for PBXNativeTarget "OneSignalOSCoreMocks" */;
@@ -3777,6 +3928,8 @@
3CEE934D2B7C73B6008440BD /* PBXTargetDependency */,
475F47232B8E398E00EC05B3 /* PBXTargetDependency */,
3C8544BB2C5AEFF700F542A9 /* PBXTargetDependency */,
+ 3C7021E62ECF0821001768C6 /* PBXTargetDependency */,
+ 3C7022212ECF124B001768C6 /* PBXTargetDependency */,
);
name = UnitTestApp;
productName = UnitTestApp;
@@ -3819,7 +3972,7 @@
};
3C01518D2C2E298E0079E076 = {
CreatedOnToolsVersion = 15.2;
- LastSwiftMigration = 1520;
+ LastSwiftMigration = 1640;
TestTargetID = DEF5CCF02539321A0003E9CC;
};
3C115160289A259500565C41 = {
@@ -3827,6 +3980,10 @@
DevelopmentTeam = 99SW8E36CT;
ProvisioningStyle = Automatic;
};
+ 3C70221B2ECF124B001768C6 = {
+ CreatedOnToolsVersion = 16.4;
+ LastSwiftMigration = 1640;
+ };
3C8544B52C5AEFF600F542A9 = {
CreatedOnToolsVersion = 15.2;
LastSwiftMigration = 1520;
@@ -3980,6 +4137,7 @@
3CC063992B6D7A8C002BB07F /* OneSignalCoreMocks */,
3C8544B52C5AEFF600F542A9 /* OneSignalOSCoreMocks */,
3CC063DC2B6D7F2A002BB07F /* OneSignalUserMocks */,
+ 3C70221B2ECF124B001768C6 /* OneSignalInAppMessagesMocks */,
3CC063A02B6D7A8D002BB07F /* OneSignalCoreTests */,
3CC063EA2B6D7FE8002BB07F /* OneSignalUserTests */,
473542492B8F93330016DB4C /* OneSignalLiveActivitiesTests */,
@@ -4006,6 +4164,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 3C70221A2ECF124B001768C6 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
3C8544B42C5AEFF600F542A9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -4229,7 +4394,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 3C30FE362F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift in Sources */,
+ 3CAA4BB72F0BAFBA00A16682 /* TriggerTests.swift in Sources */,
+ 3C7021E92ECF0CF4001768C6 /* IAMIntegrationTests.swift in Sources */,
3C01519C2C2E29F90079E076 /* IAMRequestTests.m in Sources */,
+ 3CB35FCB2F0FA20B000E6E0F /* OSMessagingControllerUserStateTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -4264,10 +4433,19 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 3C7022182ECF124B001768C6 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3C70222D2ECF12A5001768C6 /* IAMTestHelpers.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
3C8544B22C5AEFF600F542A9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 3CBB6C262ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift in Sources */,
3C8544C32C5AF18B00F542A9 /* OSCoreMocks.swift in Sources */,
3CF11E402C6E6DE2002856F5 /* MockNewRecordsState.swift in Sources */,
);
@@ -4311,6 +4489,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 3CB331682F281679000E1801 /* CustomEventsIntegrationTests.swift in Sources */,
3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */,
DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */,
DE3568F22C8911EA00AF447C /* IdentityExecutorTests.swift in Sources */,
@@ -4318,6 +4497,7 @@
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */,
3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */,
DE3568F02C89067400AF447C /* SubscriptionsExecutorTests.swift in Sources */,
+ 3CB3316A2F281692000E1801 /* OSCustomEventsExecutorTests.swift in Sources */,
3CDE664C2BFC2A56006DA114 /* OneSignalUserObjcTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -4355,6 +4535,7 @@
buildActionMask = 2147483647;
files = (
4735424D2B8F93340016DB4C /* OSLiveActivitiesExecutorTests.swift in Sources */,
+ 3C64C3322F1066D700693230 /* LiveActivitiesManagerTests.swift in Sources */,
47278E472BD92B4B00562820 /* DefaultLiveActivityAttributesTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -4363,17 +4544,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 475F473C2B8E39F300EC05B3 /* OSLiveActivitiesExtension.swift in Sources */,
- 47278E452BD7E62B00562820 /* DefaultLiveActivityAttributes.swift in Sources */,
- 475F47432B8E3A0A00EC05B3 /* OSRequestSetStartToken.swift in Sources */,
- 475F473B2B8E39F300EC05B3 /* OneSignalLiveActivityAttributes.swift in Sources */,
- 475F47362B8E39DD00EC05B3 /* OSLiveActivitiesExecutor.swift in Sources */,
- 475F473A2B8E39F300EC05B3 /* OneSignalLiveActivitiesManagerImpl.swift in Sources */,
- 475F47422B8E3A0A00EC05B3 /* OSRequestRemoveUpdateToken.swift in Sources */,
- 475F47452B8E3A0A00EC05B3 /* OSLiveActivityRequest.swift in Sources */,
- 47A885CD2BB317B300ED91FA /* AnyCodable.swift in Sources */,
- 475F47462B8E3A0A00EC05B3 /* OSRequestRemoveStartToken.swift in Sources */,
- 475F47442B8E3A0A00EC05B3 /* OSRequestSetUpdateToken.swift in Sources */,
+ 3CFA8F4F2E9087DB00201FE5 /* AnyCodable.swift in Sources */,
+ 3C3D8D782E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift in Sources */,
+ 3C3D34E92E95EAA5006A2924 /* LiveActivityConstants.swift in Sources */,
+ 3CFA8F502E9087DB00201FE5 /* OSLiveActivitiesExecutor.swift in Sources */,
+ 3CFA8F512E9087DB00201FE5 /* DefaultLiveActivityAttributes.swift in Sources */,
+ 3CFA8F522E9087DB00201FE5 /* OSRequestSetStartToken.swift in Sources */,
+ 3CFA8F532E9087DB00201FE5 /* OSRequestRemoveStartToken.swift in Sources */,
+ 3CFA8F542E9087DB00201FE5 /* OSLiveActivitiesExtension.swift in Sources */,
+ 3CFA8F552E9087DB00201FE5 /* OneSignalLiveActivitiesManagerImpl.swift in Sources */,
+ 3CFA8F562E9087DB00201FE5 /* OSRequestSetUpdateToken.swift in Sources */,
+ 3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */,
+ 3CFA8F572E9087DB00201FE5 /* OSRequestRemoveUpdateToken.swift in Sources */,
+ 3CFA8F582E9087DB00201FE5 /* OSLiveActivityRequest.swift in Sources */,
+ 3CFA8F592E9087DB00201FE5 /* OneSignalLiveActivityAttributes.swift in Sources */,
+ 3CFA8F5B2E9091A200201FE5 /* OSRequestLiveActivityReceiveReceipts.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -4420,6 +4605,7 @@
4529DED21FA81EA800CEAB1D /* NSObjectOverrider.m in Sources */,
CA42CAC320D99CB90001F2F2 /* ProvisionalAuthorizationTests.m in Sources */,
5B58E4F8237CE7B4009401E0 /* UIDeviceOverrider.m in Sources */,
+ 3C4319092F4CE9D90075492D /* SessionEndOutcomesRequestTests.swift in Sources */,
CA8E19022193C6B0009DA223 /* InAppMessagingIntegrationTests.m in Sources */,
CAB4112B20852E4C005A70D1 /* DelayedConsentInitializationParameters.m in Sources */,
7AECE59223674A9700537907 /* OSAttributedFocusTimeProcessor.m in Sources */,
@@ -4486,8 +4672,10 @@
3CEE90A72BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift in Sources */,
3C9AD6C12B22886600BC1540 /* OSRequestUpdateSubscription.swift in Sources */,
3C3130E02CA383F800906665 /* OSUser.swift in Sources */,
+ 3C68EFE72D931BA600F0896B /* OSRequestCustomEvents.swift in Sources */,
3C0EF49E28A1DBCB00E5434B /* OSUserInternalImpl.swift in Sources */,
3C8E6DFF28AB09AE0031E48A /* OSPropertyOperationExecutor.swift in Sources */,
+ 3C68EFE52D93195E00F0896B /* OSCustomEventsExecutor.swift in Sources */,
3C9AD6CB2B228B5200BC1540 /* OSRequestIdentifyUser.swift in Sources */,
3CF807372C80F3B5003E5FE1 /* OSUserUtils.swift in Sources */,
3C9AD6BC2B2285FB00BC1540 /* OSUserExecutor.swift in Sources */,
@@ -4683,6 +4871,21 @@
target = 3C115160289A259500565C41 /* OneSignalOSCore */;
targetProxy = 3C115199289AF86C00565C41 /* PBXContainerItemProxy */;
};
+ 3C60BB9E2ECF860600C765F7 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DEBAAE272A4211D900BF2C1C /* OneSignalInAppMessages */;
+ targetProxy = 3C60BB9D2ECF860600C765F7 /* PBXContainerItemProxy */;
+ };
+ 3C7021E62ECF0821001768C6 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 3E2400371D4FFC31008BDE70 /* OneSignalFramework */;
+ targetProxy = 3C7021E52ECF0821001768C6 /* PBXContainerItemProxy */;
+ };
+ 3C7022212ECF124B001768C6 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 3C70221B2ECF124B001768C6 /* OneSignalInAppMessagesMocks */;
+ targetProxy = 3C7022202ECF124B001768C6 /* PBXContainerItemProxy */;
+ };
3C7A39C42B7BED900082665E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3CC063992B6D7A8C002BB07F /* OneSignalCoreMocks */;
@@ -5066,6 +5269,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_OBJC_BRIDGING_HEADER = "OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp";
@@ -5119,6 +5323,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_OBJC_BRIDGING_HEADER = "OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -5166,6 +5371,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_OBJC_BRIDGING_HEADER = "OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp";
@@ -5300,6 +5506,194 @@
};
name = Debug;
};
+ 3C7022242ECF124B001768C6 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 99SW8E36CT;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Hiptic. All rights reserved.";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalInAppMessagesMocks;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 6.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Release;
+ };
+ 3C7022252ECF124B001768C6 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 99SW8E36CT;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Hiptic. All rights reserved.";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalInAppMessagesMocks;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 6.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Debug;
+ };
+ 3C7022262ECF124B001768C6 /* Test */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = 99SW8E36CT;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Hiptic. All rights reserved.";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalInAppMessagesMocks;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 6.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Test;
+ };
3C8544BE2C5AEFF800F542A9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -6938,7 +7332,7 @@
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ANALYZER_NONNULL = YES;
- CLANG_ENABLE_CODE_COVERAGE = NO;
+ CLANG_ENABLE_CODE_COVERAGE = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_MODULE_DEBUGGING = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
@@ -6952,6 +7346,7 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_TESTABILITY = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = NO;
INFOPLIST_FILE = OneSignalFramework/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -7119,7 +7514,7 @@
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = YES;
CLANG_ANALYZER_NONNULL = YES;
- CLANG_ENABLE_CODE_COVERAGE = NO;
+ CLANG_ENABLE_CODE_COVERAGE = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_MODULE_DEBUGGING = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
@@ -7133,6 +7528,7 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_TESTABILITY = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = NO;
INFOPLIST_FILE = OneSignalFramework/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -8981,6 +9377,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ 3C7022282ECF124B001768C6 /* Build configuration list for PBXNativeTarget "OneSignalInAppMessagesMocks" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 3C7022242ECF124B001768C6 /* Release */,
+ 3C7022252ECF124B001768C6 /* Debug */,
+ 3C7022262ECF124B001768C6 /* Test */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
3C8544C12C5AEFF800F542A9 /* Build configuration list for PBXNativeTarget "OneSignalOSCoreMocks" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClientError.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClientError.m
index e096eab13..fd0968407 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClientError.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClientError.m
@@ -46,4 +46,8 @@ - (instancetype)initWithCode:(NSInteger)code message:(NSString* _Nonnull)message
return self;
}
+- (NSString *)description {
+ return [NSString stringWithFormat:@"", (long)_code, _message, _response, _underlyingError];
+}
+
@end
diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/Categories/NSString+OneSignal.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/Categories/NSString+OneSignal.h
index c9ee3aa87..1b48a86e6 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/Categories/NSString+OneSignal.h
+++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/Categories/NSString+OneSignal.h
@@ -31,9 +31,7 @@
#define NSString_OneSignal_h
@interface NSString (OneSignal)
-- (NSString *_Nonnull)one_getVersionForRange:(NSRange)range;
- (NSString *_Nonnull)one_substringAfter:(NSString *_Nonnull)needle;
-- (NSString *_Nonnull)one_getSemanticVersion;
- (NSString *_Nullable)fileExtensionForMimeType;
- (NSString *_Nullable)supportedFileExtension;
diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/Categories/NSString+OneSignal.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/Categories/NSString+OneSignal.m
index 79b0d0c12..b8fe1fab4 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/Categories/NSString+OneSignal.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/Categories/NSString+OneSignal.m
@@ -41,30 +41,6 @@ - (NSString *)one_substringAfter:(NSString *)needle
}
-- (NSString*)one_getVersionForRange:(NSRange)range {
-
- unichar myBuffer[2];
- [self getCharacters:myBuffer range:range];
- NSString *ver = [NSString stringWithCharacters:myBuffer length:2];
- if([ver hasPrefix:@"0"]){
- return [ver one_substringAfter:@"0"];
- }
- else{
- return ver;
- }
-}
-
-- (NSString*)one_getSemanticVersion {
-
- NSMutableString *tmpstr = [[NSMutableString alloc] initWithCapacity:5];
-
- for ( int i = 0; i <=4; i+=2 ){
- [tmpstr appendString:[self one_getVersionForRange:NSMakeRange(i, 2)]];
- if (i != 4)[tmpstr appendString:@"."];
- }
-
- return (NSString*)tmpstr;
-}
- (NSString *)supportedFileExtension {
NSArray *components = [self componentsSeparatedByString:@"."];
diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h
index c57b28853..40db85978 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h
+++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h
@@ -161,6 +161,9 @@
#define GDPR_CONSENT_GRANTED @"GDPR_CONSENT_GRANTED"
#define ONESIGNAL_REQUIRE_PRIVACY_CONSENT @"OneSignal_require_privacy_consent"
+// Swizzling
+#define ONESIGNAL_DISABLE_SWIZZLING @"OneSignal_disable_swizzling"
+
// Badge handling
#define ONESIGNAL_DISABLE_BADGE_CLEARING @"OneSignal_disable_badge_clearing"
#define ONESIGNAL_APP_GROUP_NAME_KEY @"OneSignal_app_groups_key"
@@ -206,11 +209,12 @@ typedef enum {ATTRIBUTED, NOT_ATTRIBUTED} FocusAttributionState;
#define focusAttributionStateString(enum) [@[@"ATTRIBUTED", @"NOT_ATTRIBUTED"] objectAtIndex:enum]
// OneSignal Background Task Identifiers
-#define ATTRIBUTED_FOCUS_TASK @"ATTRIBUTED_FOCUS_TASK"
+#define SESSION_OUTCOMES_TASK @"SESSION_OUTCOMES_TASK"
#define OPERATION_REPO_BACKGROUND_TASK @"OPERATION_REPO_BACKGROUND_TASK"
#define IDENTITY_EXECUTOR_BACKGROUND_TASK @"IDENTITY_EXECUTOR_BACKGROUND_TASK_"
#define PROPERTIES_EXECUTOR_BACKGROUND_TASK @"PROPERTIES_EXECUTOR_BACKGROUND_TASK_"
#define SUBSCRIPTION_EXECUTOR_BACKGROUND_TASK @"SUBSCRIPTION_EXECUTOR_BACKGROUND_TASK_"
+#define CUSTOM_EVENTS_EXECUTOR_BACKGROUND_TASK @"CUSTOM_EVENTS_EXECUTOR_BACKGROUND_TASK_"
// OneSignal constants
#define OS_PUSH @"push"
@@ -347,6 +351,8 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP
#define OS_REMOVE_SUBSCRIPTION_DELTA @"OS_REMOVE_SUBSCRIPTION_DELTA"
#define OS_UPDATE_SUBSCRIPTION_DELTA @"OS_UPDATE_SUBSCRIPTION_DELTA"
+#define OS_CUSTOM_EVENT_DELTA @"OS_CUSTOM_EVENT_DELTA"
+
// Operation Repo
#define OS_OPERATION_REPO @"OS_OPERATION_REPO"
#define OS_OPERATION_REPO_DELTA_QUEUE_KEY @"OS_OPERATION_REPO_DELTA_QUEUE_KEY"
@@ -378,8 +384,16 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP
#define OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY"
#define OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY"
+// Custom Events Executor
+#define OS_CUSTOM_EVENTS_EXECUTOR @"OS_CUSTOM_EVENTS_EXECUTOR"
+#define OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY"
+#define OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY"
+#define OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY"
+
// Live Activies Executor
#define OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY"
#define OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY"
+#define OS_LIVE_ACTIVITIES_EXECUTOR_RECEIVE_RECEIPTS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_RECEIVE_RECEIPTS_KEY"
+#define OS_LIVE_ACTIVITIES_EXECUTOR_CLICKED_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_CLICKED_KEY"
#endif /* OneSignalCommonDefines_h */
diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h
index f3922e961..c91e282e4 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h
+++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h
@@ -41,7 +41,6 @@
#import
#import
#import
-#import
#import
#import
#import
diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCoreHelper.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCoreHelper.h
index 9194ead16..f3c1d4147 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCoreHelper.h
+++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCoreHelper.h
@@ -35,8 +35,6 @@
+ (void)dispatch_async_on_main_queue:(void(^)())block;
+ (void)performSelector:(SEL)aSelector onMainThreadOnObject:(id)targetObj withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;
-+ (NSString*)hashUsingSha1:(NSString*)string;
-+ (NSString*)hashUsingMD5:(NSString*)string;
+ (NSString*)trimURLSpacing:(NSString*)url;
+ (NSString*)parseNSErrorAsJsonString:(NSError*)error;
+ (BOOL)isOneSignalPayload:(NSDictionary *)payload;
diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCoreHelper.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCoreHelper.m
index 4c04fda85..9ce42e159 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCoreHelper.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCoreHelper.m
@@ -32,129 +32,9 @@
extern "C" {
#endif
-#define CC_DIGEST_DEPRECATION_WARNING \
- "This function is cryptographically broken and should not be used in security contexts. Clients should migrate to SHA256 (or stronger)."
-
-/*
- * For compatibility with legacy implementations, the *Init(), *Update(),
- * and *Final() functions declared here *always* return a value of 1 (one).
- * This corresponds to "success" in the similar openssl implementations.
- * There are no errors of any kind which can be, or are, reported here,
- * so you can safely ignore the return values of all of these functions
- * if you are implementing new code.
- *
- * The one-shot functions (CC_MD2(), CC_SHA1(), etc.) perform digest
- * calculation and place the result in the caller-supplied buffer
- * indicated by the md parameter. They return the md parameter.
- * Unlike the opensssl counterparts, these one-shot functions require
- * a non-NULL md pointer. Passing in NULL for the md parameter
- * results in a NULL return and no digest calculation.
- */
-
typedef uint32_t CC_LONG; /* 32 bit unsigned integer */
typedef uint64_t CC_LONG64; /* 64 bit unsigned integer */
-/*** MD2 ***/
-
-#define CC_MD2_DIGEST_LENGTH 16 /* digest length in bytes */
-#define CC_MD2_BLOCK_BYTES 64 /* block size in bytes */
-#define CC_MD2_BLOCK_LONG (CC_MD2_BLOCK_BYTES / sizeof(CC_LONG))
-
-typedef struct CC_MD2state_st
-{
- int num;
- unsigned char data[CC_MD2_DIGEST_LENGTH];
- CC_LONG cksm[CC_MD2_BLOCK_LONG];
- CC_LONG state[CC_MD2_BLOCK_LONG];
-} CC_MD2_CTX;
-
-extern int CC_MD2_Init(CC_MD2_CTX *c)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-extern int CC_MD2_Update(CC_MD2_CTX *c, const void *data, CC_LONG len)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-extern int CC_MD2_Final(unsigned char *md, CC_MD2_CTX *c)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-extern unsigned char *CC_MD2(const void *data, CC_LONG len, unsigned char *md)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-/*** MD4 ***/
-
-#define CC_MD4_DIGEST_LENGTH 16 /* digest length in bytes */
-#define CC_MD4_BLOCK_BYTES 64 /* block size in bytes */
-#define CC_MD4_BLOCK_LONG (CC_MD4_BLOCK_BYTES / sizeof(CC_LONG))
-
-typedef struct CC_MD4state_st
-{
- CC_LONG A,B,C,D;
- CC_LONG Nl,Nh;
- CC_LONG data[CC_MD4_BLOCK_LONG];
- uint32_t num;
-} CC_MD4_CTX;
-
-extern int CC_MD4_Init(CC_MD4_CTX *c)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-extern int CC_MD4_Update(CC_MD4_CTX *c, const void *data, CC_LONG len)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-extern int CC_MD4_Final(unsigned char *md, CC_MD4_CTX *c)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-extern unsigned char *CC_MD4(const void *data, CC_LONG len, unsigned char *md)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-
-/*** MD5 ***/
-
-#define CC_MD5_DIGEST_LENGTH 16 /* digest length in bytes */
-#define CC_MD5_BLOCK_BYTES 64 /* block size in bytes */
-#define CC_MD5_BLOCK_LONG (CC_MD5_BLOCK_BYTES / sizeof(CC_LONG))
-
-typedef struct CC_MD5state_st
-{
- CC_LONG A,B,C,D;
- CC_LONG Nl,Nh;
- CC_LONG data[CC_MD5_BLOCK_LONG];
- int num;
-} CC_MD5_CTX;
-
-extern int CC_MD5_Init(CC_MD5_CTX *c)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-extern int CC_MD5_Update(CC_MD5_CTX *c, const void *data, CC_LONG len)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-extern int CC_MD5_Final(unsigned char *md, CC_MD5_CTX *c)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-extern unsigned char *CC_MD5(const void *data, CC_LONG len, unsigned char *md)
-API_DEPRECATED(CC_DIGEST_DEPRECATION_WARNING, macos(10.4, 10.15), ios(2.0, 13.0));
-
-/*** SHA1 ***/
-
-#define CC_SHA1_DIGEST_LENGTH 20 /* digest length in bytes */
-#define CC_SHA1_BLOCK_BYTES 64 /* block size in bytes */
-#define CC_SHA1_BLOCK_LONG (CC_SHA1_BLOCK_BYTES / sizeof(CC_LONG))
-
-typedef struct CC_SHA1state_st
-{
- CC_LONG h0,h1,h2,h3,h4;
- CC_LONG Nl,Nh;
- CC_LONG data[CC_SHA1_BLOCK_LONG];
- int num;
-} CC_SHA1_CTX;
-
-extern int CC_SHA1_Init(CC_SHA1_CTX *c);
-
-extern int CC_SHA1_Update(CC_SHA1_CTX *c, const void *data, CC_LONG len);
-
-extern int CC_SHA1_Final(unsigned char *md, CC_SHA1_CTX *c);
-
-extern unsigned char *CC_SHA1(const void *data, CC_LONG len, unsigned char *md);
-
/*** SHA224 ***/
#define CC_SHA224_DIGEST_LENGTH 28 /* digest length in bytes */
#define CC_SHA224_BLOCK_BYTES 64 /* block size in bytes */
@@ -258,26 +138,6 @@ + (void)performSelector:(SEL)aSelector onMainThreadOnObject:(nullable id)targetO
}];
}
-+ (NSString*)hashUsingSha1:(NSString*)string {
- const char *cstr = [string UTF8String];
- uint8_t digest[CC_SHA1_DIGEST_LENGTH];
- CC_SHA1(cstr, (CC_LONG)strlen(cstr), digest);
- NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];
- for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++)
- [output appendFormat:@"%02x", digest[i]];
- return output;
-}
-
-+ (NSString*)hashUsingMD5:(NSString*)string {
- const char *cstr = [string UTF8String];
- uint8_t digest[CC_MD5_DIGEST_LENGTH];
- CC_MD5(cstr, (CC_LONG)strlen(cstr), digest);
- NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
- for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
- [output appendFormat:@"%02x", digest[i]];
- return output;
-}
-
+ (NSString*)trimURLSpacing:(NSString*)url {
if (!url || [url isEqual:[NSNull null]]) {
return nil;
diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h
index bc860f909..8977c214d 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h
+++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalLog.h
@@ -48,13 +48,13 @@ typedef NS_ENUM(NSUInteger, ONE_S_LOG_LEVEL) {
@protocol OSDebug
/**
The log level the OneSignal SDK should be writing to the Xcode log. Defaults to [LogLevel.WARN].
-
+
WARNING: This should not be set higher than LogLevel.WARN in a production setting.
*/
+ (void)setLogLevel:(ONE_S_LOG_LEVEL)logLevel;
/**
The log level the OneSignal SDK should be showing as a modal. Defaults to [LogLevel.NONE].
-
+
WARNING: This should not be used in a production setting.
*/
+ (void)setAlertLevel:(ONE_S_LOG_LEVEL)logLevel NS_REFINED_FOR_SWIFT;
@@ -62,7 +62,7 @@ typedef NS_ENUM(NSUInteger, ONE_S_LOG_LEVEL) {
/**
Add a listener to receive all logging messages the SDK produces.
Useful to capture and send logs to your server.
-
+
NOTE: All log messages are always passed, LogLevel has no effect on this.
*/
+ (void)addLogListener:(NSObject*_Nonnull)listener NS_REFINED_FOR_SWIFT;
diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift
index 82f951094..36ef44424 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift
+++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift
@@ -202,9 +202,15 @@ extension MockOneSignalClient {
return found
}
- public func hasExecutedRequestOfType(_ type: AnyClass) -> Bool {
- executedRequests.contains { request in
+ public func hasExecutedRequestOfType(_ type: AnyClass, expectedCount: Int? = nil) -> Bool {
+ let matchingCount = executedRequests.filter { request in
request.isKind(of: type)
+ }.count
+
+ if let expectedCount = expectedCount {
+ return matchingCount == expectedCount
+ } else {
+ return matchingCount > 0
}
}
}
diff --git a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalAttachmentHandler.m b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalAttachmentHandler.m
index 042b28146..e955a408e 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalAttachmentHandler.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalAttachmentHandler.m
@@ -28,6 +28,7 @@ of this software and associated documentation files (the "Software"), to deal
#import
#import "OneSignalAttachmentHandler.h"
#import "OneSignalNotificationCategoryController.h"
+#import "OSMacros.h"
@interface DirectDownloadDelegate : NSObject {
NSError* error;
diff --git a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionRequests.m b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionRequests.m
index ebf505112..079a129cf 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionRequests.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionRequests.m
@@ -27,6 +27,7 @@ of this software and associated documentation files (the "Software"), to deal
#import
#import "OneSignalExtensionRequests.h"
+#import "OSMacros.h"
@implementation OSRequestReceiveReceipts
diff --git a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalNotificationServiceExtensionHandler.m b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalNotificationServiceExtensionHandler.m
index 39df2b8a4..84273fe38 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalNotificationServiceExtensionHandler.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalNotificationServiceExtensionHandler.m
@@ -26,6 +26,7 @@
*/
#import
+#import "OSMacros.h"
#import
#import "OneSignalNotificationServiceExtensionHandler.h"
#import "OneSignalExtensionBadgeHandler.h"
diff --git a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalReceiveReceiptsController.m b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalReceiveReceiptsController.m
index 63aa9dcab..86e1db286 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalReceiveReceiptsController.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalReceiveReceiptsController.m
@@ -28,6 +28,7 @@
#import
#import "OneSignalReceiveReceiptsController.h"
#import
+#import "OSMacros.h"
#import "OneSignalExtensionRequests.h"
@implementation OneSignalReceiveReceiptsController
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSDynamicTriggerController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSDynamicTriggerController.m
index 2e98511bb..6db4635df 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSDynamicTriggerController.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSDynamicTriggerController.m
@@ -30,6 +30,7 @@
#import "OneSignalCommonDefines.h"
#import "OSMessagingController.h"
#import
+#import "OSMacros.h"
#import "OSSessionManager.h"
@interface OSDynamicTriggerController ()
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSInAppMessageController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSInAppMessageController.m
index b78b99d67..bf42b381b 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSInAppMessageController.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSInAppMessageController.m
@@ -27,6 +27,7 @@
#import "OSInAppMessageController.h"
#import
+#import "OSMacros.h"
#import
#import "OSInAppMessagingDefines.h"
#import "OSInAppMessagingRequests.h"
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSInAppMessageMigrationController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSInAppMessageMigrationController.m
index 001dc8569..ea3189f56 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSInAppMessageMigrationController.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSInAppMessageMigrationController.m
@@ -28,6 +28,7 @@ of this software and associated documentation files (the "Software"), to deal
#import "OSInAppMessageMigrationController.h"
#import "OSInAppMessagingDefines.h"
#import "OSInAppMessageInternal.h"
+#import "OSMacros.h"
@implementation OSInAppMessageMigrationController
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.h
index 2cae45c38..9f92ba020 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.h
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.h
@@ -61,8 +61,6 @@ NS_ASSUME_NONNULL_BEGIN
- (void)addTriggers:(NSDictionary *)triggers;
- (void)removeTriggersForKeys:(NSArray *)keys;
- (void)clearTriggers;
-- (NSDictionary *)getTriggers;
-- (id)getTriggerValueForKey:(NSString *)key;
- (void)addInAppMessageClickListener:(NSObject *_Nullable)listener;
- (void)removeInAppMessageClickListener:(NSObject *_Nullable)listener;
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m
index 3fdf402c9..80dc3f31b 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m
@@ -26,9 +26,10 @@
*/
#import "OSMessagingController.h"
-#import "UIApplication+OneSignal.h" // Previously imported via "OneSignalHelper.h"
-#import "NSDateFormatter+OneSignal.h" // Previously imported via "OneSignalHelper.h"
+#import "UIApplication+OneSignal.h"
+#import "NSDateFormatter+OneSignal.h"
#import
+#import "OSMacros.h"
#import "OSInAppMessageClickResult.h"
#import "OSInAppMessageClickEvent.h"
#import "OSInAppMessageController.h"
@@ -145,6 +146,12 @@ @interface OSMessagingController ()
@property (nonatomic) BOOL calledLoadTags;
+/// Tracks whether the first IAM fetch has completed since this cold start
+@property (nonatomic) BOOL hasCompletedFirstFetch;
+
+/// Tracks trigger keys added early on cold start (before first fetch completes), for redisplay logic
+@property (strong, nonatomic, nonnull) NSMutableSet *earlySessionTriggers;
+
@end
@implementation OSMessagingController
@@ -180,6 +187,7 @@ + (OSMessagingController *)sharedInstance {
return sharedInstance;
}
+/// Note: This method is used in tests only.
+ (void)removeInstance {
sharedInstance = nil;
once = 0;
@@ -229,6 +237,8 @@ - (instancetype)init {
self.messageDisplayQueue = [NSMutableArray new];
self.clickListeners = [NSMutableArray new];
self.lifecycleListeners = [NSMutableArray new];
+ self.hasCompletedFirstFetch = NO;
+ self.earlySessionTriggers = [NSMutableSet new];
let standardUserDefaults = OneSignalUserDefaults.initStandard;
@@ -289,7 +299,8 @@ - (void)getInAppMessagesFromServer {
// We also need the onesignal ID for ryw consistency
NSString *onesignalId = OneSignalUserManagerImpl.sharedInstance.onesignalId;
if (!onesignalId) {
- [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Failed to get in app messages due to no OneSignal ID"];
+ [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"Failed to get in app messages due to no OneSignal ID, will reattempt"];
+ shouldRetryGetInAppMessagesOnUserChange = true;
return;
}
@@ -475,6 +486,23 @@ - (void)updateInAppMessagesFromServer:(NSArray *)newMe
self.messages = newMessages;
self.calledLoadTags = NO;
[self resetRedisplayMessagesBySession];
+
+ // Apply isTriggerChanged for messages that match triggers added too early on cold start
+ if (self.earlySessionTriggers.count > 0) {
+ [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"Processing triggers added early on cold start: %@", self.earlySessionTriggers]];
+ for (OSInAppMessageInternal *message in self.messages) {
+ if ([self.redisplayedInAppMessages objectForKey:message.messageId] &&
+ [self.triggerController hasSharedTriggers:message newTriggersKeys:self.earlySessionTriggers.allObjects]) {
+ [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"Setting isTriggerChanged=YES for message %@", message]];
+ message.isTriggerChanged = YES;
+ }
+ }
+ [self.earlySessionTriggers removeAllObjects];
+ }
+
+ // Mark that first fetch has completed
+ self.hasCompletedFirstFetch = YES;
+
[self evaluateMessages];
[self deleteOldRedisplayedInAppMessages];
}
@@ -877,6 +905,13 @@ - (void)evaluateRedisplayedInAppMessages:(NSArray *)newTriggersKeys
#pragma mark Trigger Methods
- (void)addTriggers:(NSDictionary *)triggers {
[self evaluateRedisplayedInAppMessages:triggers.allKeys];
+
+ // Track triggers added early on cold start (before first fetch completes) for redisplay logic
+ if (!self.hasCompletedFirstFetch) {
+ [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"Tracking triggers added early on cold start: %@", triggers]];
+ [self.earlySessionTriggers addObjectsFromArray:triggers.allKeys];
+ }
+
[self.triggerController addTriggers:triggers];
}
@@ -894,10 +929,6 @@ - (void)clearTriggers {
return self.triggerController.getTriggers;
}
-- (id)getTriggerValueForKey:(NSString *)key {
- return [self.triggerController getTriggerValueForKey:key];
-}
-
#pragma mark OSInAppMessageViewControllerDelegate Methods
- (void)messageViewControllerDidDisplay:(OSInAppMessageInternal *)message {
[self onDidDisplayInAppMessage:message];
@@ -1326,7 +1357,6 @@ - (void)addTriggers:(NSDictionary *)triggers {}
- (void)removeTriggersForKeys:(NSArray *)keys {}
- (void)clearTriggers {}
- (NSDictionary *)getTriggers { return @{}; }
-- (id)getTriggerValueForKey:(NSString *)key { return 0; }
#pragma mark OSInAppMessageViewControllerDelegate Methods
- (void)messageViewControllerWasDismissed {}
- (void)messageViewDidSelectAction:(OSInAppMessageInternal *)message withAction:(OSInAppMessageClickResult *)action {}
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.h
index ce99b69db..e036086d3 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.h
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.h
@@ -52,7 +52,6 @@ NS_ASSUME_NONNULL_BEGIN
- (void)addTriggers:(NSDictionary *)triggers;
- (void)removeTriggersForKeys:(NSArray *)keys;
- (NSDictionary *)getTriggers;
-- (id)getTriggerValueForKey:(NSString *)key;
- (void)timeSinceLastMessage:(NSDate *)date;
@end
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.m
index 8499192ab..80689b0e6 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.m
@@ -27,6 +27,7 @@
#import "OSTriggerController.h"
#import "OSInAppMessagingDefines.h"
+#import "OSMacros.h"
@interface OSTriggerController ()
@property (strong, nonatomic, nonnull) NSMutableDictionary *triggers;
@@ -69,12 +70,6 @@ - (void)removeTriggersForKeys:(NSArray *)keys {
}
}
-- (id)getTriggerValueForKey:(NSString *)key {
- @synchronized (self.triggers) {
- return self.triggers[key];
- }
-}
-
/*
* Part of redisplay logic
*
@@ -186,11 +181,9 @@ - (BOOL)messageMatchesTriggers:(OSInAppMessageInternal *)message {
- (BOOL)evaluateTrigger:(OSTrigger *)trigger forMessage:(OSInAppMessageInternal *)message {
if (!self.triggers[trigger.property] && [trigger.kind isEqualToString:OS_DYNAMIC_TRIGGER_KIND_CUSTOM]) {
// The value doesn't exist
-
- // The condition for this trigger is true since the value doesn't exist
- // Either loop to the next condition, or return true if we are the last condition
- return trigger.operatorType == OSTriggerOperatorTypeNotExists ||
- (trigger.value && trigger.operatorType == OSTriggerOperatorTypeNotEqualTo);
+
+ // Only NotExists operator should return true for non-existent values
+ return trigger.operatorType == OSTriggerOperatorTypeNotExists;
} else if (trigger.operatorType == OSTriggerOperatorTypeExists) {
return true;
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageBridgeEvent.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageBridgeEvent.m
index 52c5f1c79..3aba051f0 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageBridgeEvent.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageBridgeEvent.m
@@ -27,6 +27,7 @@
#import "OSInAppMessageBridgeEvent.h"
#import "OSInAppMessageClickResult.h"
+#import "OSMacros.h"
@implementation OSInAppMessageBridgeEvent
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageClickEvent.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageClickEvent.m
index ee6da0999..a949de9bc 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageClickEvent.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageClickEvent.m
@@ -27,6 +27,7 @@
#import "OSInAppMessageClickEvent.h"
+#import "OSMacros.h"
@implementation OSInAppMessageClickEvent
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageClickResult.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageClickResult.m
index 24a479bdf..676375dbd 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageClickResult.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageClickResult.m
@@ -28,6 +28,7 @@
#import "OSInAppMessageClickResult.h"
#import "OSInAppMessagePushPrompt.h"
#import "OSInAppMessageLocationPrompt.h"
+#import "OSMacros.h"
@implementation OSInAppMessageClickResult
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageDisplayStats.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageDisplayStats.m
index c71c73e05..3d829dbc6 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageDisplayStats.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageDisplayStats.m
@@ -26,6 +26,7 @@
*/
#import "OSInAppMessageDisplayStats.h"
+#import "OSMacros.h"
@interface OSInAppMessageDisplayStats ()
@property (nonatomic, readwrite) BOOL redisplayEnabled;
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageInternal.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageInternal.m
index 040e3bc54..8875f53e6 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageInternal.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageInternal.m
@@ -28,6 +28,7 @@
#import "OSInAppMessageInternal.h"
#import "NSDateFormatter+OneSignal.h"
#import "OneSignalCommonDefines.h"
+#import "OSMacros.h"
@implementation OSInAppMessage
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageLocationPrompt.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageLocationPrompt.m
index fb0513cbb..b5d0e8d60 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageLocationPrompt.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageLocationPrompt.m
@@ -29,6 +29,7 @@
#import "OSInAppMessageLocationPrompt.h"
#import
#import
+#import "OSMacros.h"
//@interface OneSignalLocation ()
//
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessagePushPrompt.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessagePushPrompt.m
index 59bd00c38..18e135ff6 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessagePushPrompt.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessagePushPrompt.m
@@ -28,6 +28,7 @@
#import "OSInAppMessagePushPrompt.h"
#import "OneSignalInAppMessages.h"
#import
+#import "OSMacros.h"
@implementation OSInAppMessagePushPrompt
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageTag.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageTag.m
index f2a559197..757aa2512 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageTag.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSInAppMessageTag.m
@@ -26,6 +26,7 @@
*/
#import "OSInAppMessageTag.h"
+#import "OSMacros.h"
@implementation OSInAppMessageTag
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSTrigger.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSTrigger.m
index cb964b80b..444dc73b6 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSTrigger.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Model/OSTrigger.m
@@ -26,6 +26,7 @@
*/
#import "OSTrigger.h"
+#import "OSMacros.h"
@implementation OSTrigger
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m
index e4368f0a9..bfdf50418 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m
@@ -26,8 +26,13 @@ of this software and associated documentation files (the "Software"), to deal
*/
#import
#import "OSInAppMessagingRequests.h"
+#import "OSMacros.h"
@implementation OSRequestGetInAppMessages
+- (NSString *)description {
+ return [NSString stringWithFormat:@"", self.path];
+}
+
+ (instancetype _Nonnull) withSubscriptionId:(NSString * _Nonnull)subscriptionId
withAliasLabel:(NSString * _Nonnull)aliasLabel
withAliasId:(NSString * _Nonnull)aliasId
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m
index f89832c43..e74d98d5d 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m
@@ -29,6 +29,7 @@
#import
#import "OSInAppMessageClickResult.h"
#import
+#import "OSMacros.h"
@interface OSInAppMessageView ()
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m
index 609419b94..bdefdb3f6 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m
@@ -30,6 +30,7 @@
#import "OSInAppMessageController.h"
#import "OSInAppMessageBridgeEvent.h"
#import
+#import "OSMacros.h"
#import "OSSessionManager.h"
#define HIGHEST_CONSTRAINT_PRIORITY 999.0f
@@ -515,9 +516,14 @@ - (void)dismissCurrentInAppMessage:(BOOL)up withVelocity:(double)velocity {
if (self.dismissalTimer)
[self.dismissalTimer invalidate];
- // If the rendering event never occurs any constraints being adjusted for dismissal will be nil
- // and we should bypass dismissal adjustments and animations and skip straight to the OSMessagingController callback for dismissing
- if (!self.didPageRenderingComplete) {
+ // Return early and skip constraint adjustments/animations if:
+ // - Page rendering never completed (constraints would be nil)
+ // - messageView is not valid or not a direct subview of self.view (prevents crashes when
+ // dismissal is triggered while the view hierarchy is in an inconsistent state)
+ if (!self.didPageRenderingComplete ||
+ !self.messageView ||
+ self.messageView.superview != self.view)
+ {
[self dismissViewControllerAnimated:false completion:nil];
[self.delegate messageViewControllerWasDismissed:self.message displayed:NO];
return;
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OneSignalWebView.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OneSignalWebView.m
index dbcc8e777..06910b334 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OneSignalWebView.m
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OneSignalWebView.m
@@ -28,7 +28,7 @@
#import
#import "OneSignalWebView.h"
#import
-
+#import "OSMacros.h"
@implementation OneSignalWebView
diff --git a/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.h
similarity index 100%
rename from iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.h
rename to iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.h
diff --git a/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m
similarity index 100%
rename from iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.m
rename to iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/.swiftlint.yml b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/.swiftlint.yml
new file mode 100644
index 000000000..d69b4172b
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/.swiftlint.yml
@@ -0,0 +1,2 @@
+disabled_rules:
+ - identifier_name
\ No newline at end of file
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/IAMTestHelpers.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/IAMTestHelpers.swift
new file mode 100644
index 000000000..931ad17fe
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/IAMTestHelpers.swift
@@ -0,0 +1,112 @@
+/*
+ Modified MIT License
+
+ Copyright 2025 OneSignal
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ 1. The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ 2. All copies of substantial portions of the Software may only be used in connection
+ with services provided by OneSignal.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+import Foundation
+import OneSignalInAppMessages
+
+let OS_TEST_MESSAGE_ID = "a4b3gj7f-d8cc-11e4-bed1-df8f05be55ba"
+let OS_TEST_MESSAGE_VARIANT_ID = "m8dh7234f-d8cc-11e4-bed1-df8f05be55ba"
+let OS_TEST_ENGLISH_VARIANT_ID = "11e4-bed1-df8f05be55ba-m8dh7234f-d8cc"
+
+let OS_DUMMY_HTML = "Hello World
"
+
+@objc
+public class IAMTestHelpers: NSObject {
+
+ nonisolated(unsafe) static var messageIdIncrementer = 0
+
+ /// Convert OSTriggerOperatorType enum to string
+ private static func OS_OPERATOR_TO_STRING(_ type: Int32) -> String {
+ // Trigger operator strings
+ let OS_OPERATOR_STRINGS: [String] = [
+ "greater",
+ "less",
+ "equal",
+ "not_equal",
+ "less_or_equal",
+ "greater_or_equal",
+ "exists",
+ "not_exists",
+ "in"
+ ]
+
+ return OS_OPERATOR_STRINGS[Int(type)]
+ }
+
+ /// Returns the JSON of a minimal in-app message that can be used as a building block.
+ @objc
+ public static func testDefaultMessageJson() -> [String: Any] {
+ messageIdIncrementer += 1
+ return [
+ "id": String(format: "%@_%i", OS_TEST_MESSAGE_ID, messageIdIncrementer),
+ "variants": [
+ "ios": [
+ "default": OS_TEST_MESSAGE_VARIANT_ID,
+ "en": OS_TEST_ENGLISH_VARIANT_ID
+ ],
+ "all": [
+ "default": "should_never_be_used_by_any_test"
+ ]
+ ],
+ "triggers": []
+ ]
+ }
+
+ /// Returns the JSON of an in-app message with trigger.
+ @objc
+ public static func testMessageJsonWithTrigger(kind: String, property: String, triggerId: String, type: Int32, value: Any) -> [String: Any] {
+ var testMessage = self.testDefaultMessageJson()
+
+ testMessage["triggers"] = [
+ [
+ [
+ "kind": kind,
+ "property": property,
+ "operator": OS_OPERATOR_TO_STRING(type),
+ "value": value,
+ "id": triggerId
+ ]
+ ]
+ ]
+ return testMessage
+ }
+
+ @objc
+ public static func testFetchMessagesResponse(messages: [[String: Any]]) -> [String: Any] {
+ return [
+ "in_app_messages": messages
+ ]
+ }
+
+ /// Returns the JSON of a preview or test in-app message.
+ @objc
+ public static func testMessagePreviewJson() -> [String: Any] {
+ var message = self.testDefaultMessageJson()
+ message["is_preview"] = true
+ return message
+ }
+}
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/OneSignalInAppMessagesMocks.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/OneSignalInAppMessagesMocks.h
new file mode 100644
index 000000000..ed33d0be7
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/OneSignalInAppMessagesMocks.h
@@ -0,0 +1,36 @@
+/*
+ Modified MIT License
+
+ Copyright 2025 OneSignal
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ 1. The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ 2. All copies of substantial portions of the Software may only be used in connection
+ with services provided by OneSignal.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+#import
+
+//! Project version number for OneSignalInAppMessagesMocks.
+FOUNDATION_EXPORT double OneSignalInAppMessagesMocksVersionNumber;
+
+//! Project version string for OneSignalInAppMessagesMocks.
+FOUNDATION_EXPORT const unsigned char OneSignalInAppMessagesMocksVersionString[];
+
+// In this header, you should import all the public headers of your framework using statements like #import
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/.swiftlint.yml b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/.swiftlint.yml
new file mode 100644
index 000000000..c6778fb1d
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/.swiftlint.yml
@@ -0,0 +1,3 @@
+# in tests, we may want to force cast and throw any errors
+disabled_rules:
+ - force_cast
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift
new file mode 100644
index 000000000..b32ec9192
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift
@@ -0,0 +1,460 @@
+/*
+ Modified MIT License
+
+ Copyright 2025 OneSignal
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ 1. The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ 2. All copies of substantial portions of the Software may only be used in connection
+with services provided by OneSignal.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+import XCTest
+import OneSignalOSCore
+import OneSignalCoreMocks
+import OneSignalOSCoreMocks
+import OneSignalUserMocks
+import OneSignalInAppMessagesMocks
+// @testable import OneSignalUser
+// @testable import OneSignalInAppMessages
+
+/**
+ Tests for early trigger tracking functionality.
+
+ These tests verify that in-app messages can be displayed on cold starts when their
+ triggers are added very early (before IAM fetch completes).
+ */
+final class EarlyTriggerTrackingTests: XCTestCase {
+
+ private let testSubscriptionId = "test-subscription-id-12345"
+ private let testOneSignalId = "test-onesignal-id-12345"
+ private let testAppId = "test-app-id"
+
+ override func setUpWithError() throws {
+ OneSignalCoreMocks.clearUserDefaults()
+ OneSignalUserMocks.reset()
+ OSConsistencyManager.shared.reset()
+ OSMessagingController.removeInstance()
+
+ // Set up basic configuration
+ OneSignalConfigManager.setAppId(testAppId)
+ OneSignalLog.setLogLevel(.LL_VERBOSE)
+
+ // Tell the User Manager JWT is not required so OSUserUtils.getAlias resolves
+ // (otherwise the IAM fetch is blocked by null alias and no request fires).
+ OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false)
+ }
+
+ override func tearDownWithError() throws {
+ OSMessagingController.removeInstance()
+ }
+
+ /**
+ Test that hasCompletedFirstFetch is set to true after the first fetch completes.
+
+ Scenario:
+ - Fresh start with no previous fetch
+ - First IAM fetch completes
+
+ Expected:
+ - hasCompletedFirstFetch changes from false to true
+ */
+ func testHasCompletedFirstFetch_isSetAfterFirstFetch() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OSMessagingController.start()
+ let controller = OSMessagingController.sharedInstance()
+
+ // Verify initial state
+ XCTAssertFalse(controller.hasCompletedFirstFetch)
+
+ // Set up mock responses
+ MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId)
+ ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId)
+
+ let response = IAMTestHelpers.testFetchMessagesResponse(messages: [])
+ client.setMockResponseForRequest(
+ request: "",
+ response: response)
+
+ /* Execute */
+ OneSignalUserManagerImpl.sharedInstance.start()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
+
+ /* Verify */
+ XCTAssertTrue(controller.hasCompletedFirstFetch)
+ }
+
+ /**
+ Test that triggers added before the first fetch completes are tracked in earlySessionTriggers.
+
+ Scenario:
+ - Cold start, no IAM fetch has completed yet
+ - Triggers are added very early in the app lifecycle
+
+ Expected:
+ - hasCompletedFirstFetch is false
+ - Triggers are added to earlySessionTriggers
+ */
+ func testTriggersAddedBeforeFirstFetch_areTrackedInEarlySessionTriggers() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OSMessagingController.start()
+ let controller = OSMessagingController.sharedInstance()
+
+ // Verify initial state
+ XCTAssertFalse(controller.hasCompletedFirstFetch)
+ XCTAssertEqual(controller.earlySessionTriggers.count, 0)
+
+ /* Execute */
+ // Add triggers before first fetch completes
+ controller.addTriggers(["trigger1": "value1", "trigger2": "value2"])
+
+ /* Verify */
+ XCTAssertFalse(controller.hasCompletedFirstFetch)
+ XCTAssertEqual(controller.earlySessionTriggers.count, 2)
+ XCTAssertTrue(controller.earlySessionTriggers.contains("trigger1"))
+ XCTAssertTrue(controller.earlySessionTriggers.contains("trigger2"))
+ }
+
+ /**
+ Test that multiple calls to addTriggers before first fetch accumulate in earlySessionTriggers.
+
+ Scenario:
+ - Multiple trigger additions before first fetch completes
+
+ Expected:
+ - All trigger keys are accumulated in earlySessionTriggers
+ */
+ func testMultipleTriggersAddedBeforeFirstFetch_areAllTracked() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OSMessagingController.start()
+ let controller = OSMessagingController.sharedInstance()
+
+ /* Execute */
+ // Add triggers in multiple calls
+ controller.addTriggers(["trigger1": "value1"])
+ controller.addTriggers(["trigger2": "value2"])
+ controller.addTriggers(["trigger3": "value3"])
+
+ /* Verify */
+ XCTAssertFalse(controller.hasCompletedFirstFetch, )
+ XCTAssertEqual(controller.earlySessionTriggers.count, 3)
+ XCTAssertTrue(controller.earlySessionTriggers.contains("trigger1"))
+ XCTAssertTrue(controller.earlySessionTriggers.contains("trigger2"))
+ XCTAssertTrue(controller.earlySessionTriggers.contains("trigger3"))
+ }
+
+ /**
+ Test that triggers added after the first fetch completes are NOT tracked in earlySessionTriggers.
+
+ Scenario:
+ - First IAM fetch has completed
+ - New triggers are added
+
+ Expected:
+ - hasCompletedFirstFetch is true
+ - Triggers are NOT added to earlySessionTriggers
+ */
+ func testTriggersAddedAfterFirstFetch_areNotTrackedInEarlySessionTriggers() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OSMessagingController.start()
+ let controller = OSMessagingController.sharedInstance()
+
+ // Set up mock responses
+ MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId)
+ ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId)
+
+ // Set up IAM fetch response with an empty message list
+ let response = IAMTestHelpers.testFetchMessagesResponse(messages: [])
+ client.setMockResponseForRequest(
+ request: "",
+ response: response)
+
+ // Start the SDK and trigger first fetch
+ OneSignalUserManagerImpl.sharedInstance.start()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2.0)
+
+ // Verify first fetch completed
+ XCTAssertTrue(controller.hasCompletedFirstFetch)
+ XCTAssertEqual(controller.earlySessionTriggers.count, 0)
+
+ /* Execute */
+ // Add triggers after first fetch
+ controller.addTriggers(["lateAction": "value"])
+
+ /* Verify */
+ XCTAssertTrue(controller.hasCompletedFirstFetch)
+ XCTAssertEqual(controller.earlySessionTriggers.count, 0)
+
+ // Verify the triggers were still added to the trigger controller
+ XCTAssertTrue(controller.triggerController.messageMatchesTriggers(
+ OSInAppMessageInternal.instance(withJson: IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "lateAction",
+ triggerId: "test_id",
+ type: 2, // equal
+ value: "value"
+ ))!
+ ))
+ }
+
+ /**
+ Test that messages matching early triggers get isTriggerChanged flag set when received from server.
+
+ Scenario:
+ - Triggers are added before first fetch
+ - IAM fetch completes with messages that were previously shown (in redisplayedInAppMessages)
+ - Some messages match the early triggers
+
+ Expected:
+ - Messages matching early triggers have isTriggerChanged = true
+ - Messages not matching early triggers have isTriggerChanged = false
+ - earlySessionTriggers is cleared after processing
+ - hasCompletedFirstFetch is true after fetch
+ */
+ func testMessagesMatchingEarlyTriggers_getIsTriggerChangedFlag() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OSMessagingController.start()
+ let controller = OSMessagingController.sharedInstance()
+
+ // Add triggers before first fetch
+ controller.addTriggers(["loginAction": "complete", "sessionStart": "true"])
+
+ // Verify triggers were tracked
+ XCTAssertEqual(controller.earlySessionTriggers.count, 2)
+ XCTAssertFalse(controller.hasCompletedFirstFetch)
+
+ // Set up mock responses
+ MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId)
+ ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId)
+
+ // Create test messages:
+ // Message 1: Matches early trigger "loginAction"
+ let message1Json = IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "loginAction",
+ triggerId: "trigger1",
+ type: 2, // equal
+ value: "complete"
+ )
+ let message1 = OSInAppMessageInternal.instance(withJson: message1Json)!
+
+ // Message 2: Matches early trigger "sessionStart"
+ let message2Json = IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "sessionStart",
+ triggerId: "trigger2",
+ type: 2, // equal
+ value: "true"
+ )
+ let message2 = OSInAppMessageInternal.instance(withJson: message2Json)!
+
+ // Message 3: Does not match any early trigger
+ let message3Json = IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "otherAction",
+ triggerId: "trigger3",
+ type: 2, // equal
+ value: "something"
+ )
+ let message3 = OSInAppMessageInternal.instance(withJson: message3Json)!
+
+ // Mark messages 1 and 2 as previously displayed (so they're in redisplayedInAppMessages)
+ controller.redisplayedInAppMessages[message1.messageId] = message1
+ controller.redisplayedInAppMessages[message2.messageId] = message2
+
+ // Set up IAM fetch response
+ let response = IAMTestHelpers.testFetchMessagesResponse(messages: [message1Json, message2Json, message3Json])
+ client.setMockResponseForRequest(
+ request: "",
+ response: response)
+
+ /* Execute */
+ // Start the SDK and trigger first fetch
+ OneSignalUserManagerImpl.sharedInstance.start()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2.0)
+
+ /* Verify */
+ // First fetch should have completed
+ XCTAssertTrue(controller.hasCompletedFirstFetch)
+
+ // Early triggers should be cleared
+ XCTAssertEqual(controller.earlySessionTriggers.count, 0)
+
+ // Messages should be received
+ XCTAssertEqual(controller.messages.count, 3)
+
+ // Cast to properly typed array and find the messages
+ let messages = controller.messages as! [OSInAppMessageInternal]
+ let receivedMessage1 = messages.first { $0.messageId == message1.messageId }
+ let receivedMessage2 = messages.first { $0.messageId == message2.messageId }
+ let receivedMessage3 = messages.first { $0.messageId == message3.messageId }
+
+ XCTAssertNotNil(receivedMessage1)
+ XCTAssertNotNil(receivedMessage2)
+ XCTAssertNotNil(receivedMessage3)
+
+ // Messages matching early triggers and in redisplayedInAppMessages should have isTriggerChanged = true
+ XCTAssertTrue(receivedMessage1!.isTriggerChanged)
+ XCTAssertTrue(receivedMessage2!.isTriggerChanged)
+
+ // Message 3 does not match early triggers (even though not in redisplayedInAppMessages anyway)
+ XCTAssertFalse(receivedMessage3!.isTriggerChanged)
+ }
+
+ /**
+ Test that messages NOT in redisplayedInAppMessages don't get isTriggerChanged flag, even if they match early triggers.
+
+ Scenario:
+ - Triggers are added before first fetch
+ - IAM fetch completes with messages that were NOT previously shown
+ - Messages match the early triggers
+
+ Expected:
+ - Messages should NOT have isTriggerChanged = true (because they weren't in redisplayedInAppMessages)
+ */
+ func testMessagesNotInRedisplayed_dontGetIsTriggerChangedFlag() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OSMessagingController.start()
+ let controller = OSMessagingController.sharedInstance()
+
+ // Add triggers before first fetch
+ controller.addTriggers(["newTrigger": "value"])
+
+ // Set up mock responses
+ MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId)
+ ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId)
+
+ // Create a test message that matches the early trigger
+ let messageJson = IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "newTrigger",
+ triggerId: "trigger1",
+ type: 2, // equal
+ value: "value"
+ )
+ let message = OSInAppMessageInternal.instance(withJson: messageJson)!
+
+ // Do NOT add this message to redisplayedInAppMessages
+
+ // Set up IAM fetch response
+ let response = IAMTestHelpers.testFetchMessagesResponse(messages: [messageJson])
+ client.setMockResponseForRequest(
+ request: "",
+ response: response)
+
+ /* Execute */
+ // Start the SDK and trigger first fetch
+ OneSignalUserManagerImpl.sharedInstance.start()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1.0)
+
+ /* Verify */
+ XCTAssertTrue(controller.hasCompletedFirstFetch)
+ XCTAssertEqual(controller.messages.count, 1)
+
+ let messages = controller.messages as! [OSInAppMessageInternal]
+ let receivedMessage = messages.first { $0.messageId == message.messageId }
+ XCTAssertNotNil(receivedMessage)
+
+ // Message should NOT have isTriggerChanged because it's not in redisplayedInAppMessages
+ XCTAssertFalse(receivedMessage!.isTriggerChanged)
+ }
+
+ /**
+ Test that earlySessionTriggers only applies to messages with shared trigger keys.
+
+ Scenario:
+ - Multiple triggers added early
+ - Messages with different trigger combinations
+
+ Expected:
+ - Only messages with matching trigger keys get isTriggerChanged
+ */
+ func testIsTriggerChanged_onlyAppliedToMessagesWithMatchingTriggerKeys() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OSMessagingController.start()
+ let controller = OSMessagingController.sharedInstance()
+
+ // Add specific triggers before first fetch
+ controller.addTriggers(["action_a": "value1"])
+
+ // Set up mock responses
+ MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId)
+ ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId)
+
+ // Message 1: Uses trigger "action_a" (matches early triggers)
+ let message1Json = IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "action_a",
+ triggerId: "trigger1",
+ type: 2,
+ value: "value1"
+ )
+ let message1 = OSInAppMessageInternal.instance(withJson: message1Json)!
+
+ // Message 2: Uses trigger "action_b" (does NOT match early triggers)
+ let message2Json = IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "action_b",
+ triggerId: "trigger2",
+ type: 2,
+ value: "value2"
+ )
+ let message2 = OSInAppMessageInternal.instance(withJson: message2Json)!
+
+ // Mark both as previously displayed
+ controller.redisplayedInAppMessages[message1.messageId] = message1
+ controller.redisplayedInAppMessages[message2.messageId] = message2
+
+ // Set up IAM fetch response
+ let response = IAMTestHelpers.testFetchMessagesResponse(messages: [message1Json, message2Json])
+ client.setMockResponseForRequest(
+ request: "",
+ response: response)
+
+ /* Execute */
+ OneSignalUserManagerImpl.sharedInstance.start()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1.0)
+
+ /* Verify */
+ let messages = controller.messages as! [OSInAppMessageInternal]
+ let receivedMessage1 = messages.first { $0.messageId == message1.messageId }
+ let receivedMessage2 = messages.first { $0.messageId == message2.messageId }
+
+ XCTAssertNotNil(receivedMessage1)
+ XCTAssertNotNil(receivedMessage2)
+
+ // Only message 1 should have isTriggerChanged (it has shared trigger "action_a")
+ XCTAssertTrue(receivedMessage1!.isTriggerChanged)
+ XCTAssertFalse(receivedMessage2!.isTriggerChanged)
+ }
+}
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift
new file mode 100644
index 000000000..8f2cdb26f
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift
@@ -0,0 +1,119 @@
+/*
+ Modified MIT License
+
+ Copyright 2025 OneSignal
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ 1. The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ 2. All copies of substantial portions of the Software may only be used in connection
+with services provided by OneSignal.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+import XCTest
+@testable import OneSignalInAppMessages
+import OneSignalOSCore
+import OneSignalUser
+import OneSignalCoreMocks
+import OneSignalOSCoreMocks
+import OneSignalUserMocks
+import OneSignalInAppMessagesMocks
+
+/**
+ These tests can include some Obj-C InAppMessagingIntegrationTests migrations.
+ */
+final class IAMIntegrationTests: XCTestCase {
+ override func setUpWithError() throws {
+ OneSignalCoreMocks.clearUserDefaults()
+ OneSignalUserMocks.reset()
+ OSConsistencyManager.shared.reset()
+ // Temp. logging to help debug during testing
+ OneSignalLog.setLogLevel(.LL_VERBOSE)
+
+ // Tell the User Manager JWT is not required so OSUserUtils.getAlias resolves
+ // (otherwise the IAM fetch is blocked by null alias and no request fires).
+ OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false)
+ }
+
+ override func tearDownWithError() throws { }
+
+ /**
+ Test IAMs should display even when IAMs are paused.
+ */
+ func testPreviewIAMIsDisplayedOnPause() throws {
+ /* Setup */
+ OneSignalCoreImpl.setSharedClient(MockOneSignalClient())
+ // App ID is set because there are guards against nil App ID
+ OneSignalConfigManager.setAppId("test-app-id")
+
+ // 1. Pause IAMs
+ OneSignalInAppMessages.__paused(true)
+
+ // 2. Create a preview message
+ let messageJson = IAMTestHelpers.testMessagePreviewJson()
+ let message = OSInAppMessageInternal.instance(withJson: messageJson)
+ XCTAssertNotNil(message, "Preview message should be created successfully")
+
+ // 3. Present the preview message
+ OSMessagingController.sharedInstance().present(inAppPreviewMessage: message)
+
+ // 4. Verify that the preview IAM is showing even when paused
+ XCTAssertTrue(OSMessagingController.sharedInstance().isInAppMessageShowing)
+ }
+
+ /**
+ Pausing IAMs will not evaluate messages.
+ */
+ func testPausingIAMs_doesNotCreateMessageQueue() throws {
+ /* Setup */
+
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+
+ // 1. App ID is set because there are guards against nil App ID
+ OneSignalConfigManager.setAppId("test-app-id")
+
+ // 2. Set up mock responses for the anonymous user, as the user needs an OSID
+ MockUserRequests.setDefaultCreateAnonUserResponses(with: client)
+
+ // 3. Set up mock responses for fetching IAMs
+ let message = IAMTestHelpers.testMessageJsonWithTrigger(kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, property: "session_time", triggerId: "test_id1", type: 1, value: 10.0)
+ let response = IAMTestHelpers.testFetchMessagesResponse(messages: [message])
+ client.setMockResponseForRequest(
+ request: "",
+ response: response)
+
+ // 4. Unblock the Consistency Manager to allow fetching of IAMs
+ ConsistencyManagerTestHelpers.setDefaultRywToken(id: anonUserOSID)
+
+ // 5. Pausing should prevent messages from being evaluated and shown
+ OneSignalInAppMessages.__paused(true)
+
+ // 6. Start the user manager to generate a user instance
+ OneSignalUserManagerImpl.sharedInstance.start()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
+
+ // 7. Fetch IAMs
+ OneSignalInAppMessages.getFromServer()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
+
+ // Make sure no IAM is showing, and the queue has no IAMs
+ XCTAssertFalse(OSMessagingController.sharedInstance().isInAppMessageShowing)
+ XCTAssertEqual(OSMessagingController.sharedInstance().messageDisplayQueue.count, 0)
+ }
+}
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift
new file mode 100644
index 000000000..346d029a1
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift
@@ -0,0 +1,174 @@
+/*
+ Modified MIT License
+
+ Copyright 2025 OneSignal
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ 1. The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ 2. All copies of substantial portions of the Software may only be used in connection
+with services provided by OneSignal.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+import XCTest
+@testable import OneSignalInAppMessages
+import OneSignalOSCore
+import OneSignalCoreMocks
+import OneSignalOSCoreMocks
+import OneSignalUserMocks
+@testable import OneSignalUser
+
+/**
+ Tests for OSMessagingController's user state observer functionality.
+
+ These tests verify that IAM fetching is deferred when blockers (push subscription, alias, or
+ OneSignal ID) are absent at fetch time, and retried when the user state observer fires with a
+ valid OneSignal ID.
+
+ Related to PR: https://github.com/OneSignal/OneSignal-iOS-SDK/pull/1626
+ */
+final class OSMessagingControllerUserStateTests: XCTestCase {
+
+ private let testOneSignalId = "test-onesignal-id-12345"
+ private let testExternalId = "test-external-id-12345"
+ private let testAppId = "test-app-id"
+
+ override func setUpWithError() throws {
+ OneSignalCoreMocks.clearUserDefaults()
+ OneSignalUserMocks.reset()
+ OSConsistencyManager.shared.reset()
+ OSMessagingController.removeInstance()
+
+ OneSignalConfigManager.setAppId(testAppId)
+ OneSignalLog.setLogLevel(.LL_VERBOSE)
+
+ // Tell the User Manager JWT is not required so OSUserUtils.getAlias resolves
+ // (otherwise the IAM fetch is blocked by null alias and no request fires).
+ OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false)
+ }
+
+ override func tearDownWithError() throws {
+ OSMessagingController.removeInstance()
+ }
+
+ /**
+ Test that getInAppMessagesFromServer does not fire an IAM network request when the
+ OneSignal ID is unavailable.
+
+ Scenario:
+ - User manager has not been started; no OneSignal ID is available
+ - getInAppMessagesFromServer is called
+
+ Expected:
+ - No IAM request fires (the call returns early)
+ */
+ func testDoesNotFetchWhenOneSignalIDUnavailable() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OSMessagingController.start()
+
+ /* Execute */
+ OneSignalInAppMessages.getFromServer()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
+
+ /* Verify */
+ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestGetInAppMessages.self, expectedCount: 0))
+ }
+
+ /**
+ Test that the IAM fetch is retried when the user state observer fires with a valid
+ OneSignal ID after a previously-deferred attempt.
+
+ Scenario:
+ - Anonymous user is created (sets push subscription ID)
+ - login() is called but the network response is delayed, so OneSignal ID is not yet hydrated
+ - getInAppMessagesFromServer is called; the fetch is deferred because OneSignal ID is missing
+ - The login response then arrives, OneSignal ID is hydrated, and the user state observer fires
+
+ Expected:
+ - No IAM request fires during the deferred call
+ - Exactly one IAM request fires after the user state observer triggers the retry
+ */
+ func testRetriesFetchWhenUserStateChangesWithValidOneSignalID() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OSMessagingController.start()
+
+ ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId)
+
+ /* Execute */
+ MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testPushSubId)
+ OneSignalUserManagerImpl.sharedInstance.start()
+
+ // Login to a new user, without setting the client response yet, so onesignal ID is not hydrated.
+ OneSignalUserManagerImpl.sharedInstance.login(externalId: testExternalId, token: nil)
+
+ // First attempt: fetch is deferred because OneSignal ID is missing on the new user.
+ OneSignalInAppMessages.getFromServer()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
+
+ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestGetInAppMessages.self, expectedCount: 0))
+
+ // Now let the login succeed, receive onesignal ID which fires user state observer.
+ MockUserRequests.setDefaultIdentifyUserResponses(with: client, externalId: testExternalId)
+ OneSignalUserManagerImpl.sharedInstance.userExecutor?.userRequestQueue.first?.sentToClient = false
+ OneSignalUserManagerImpl.sharedInstance.userExecutor?.executePendingRequests()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
+
+ /* Verify */
+ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestGetInAppMessages.self, expectedCount: 1))
+ }
+
+ /**
+ Test that a user state change without a pending retry does not trigger an extra fetch.
+
+ Scenario:
+ - Anonymous user starts with a valid OneSignal ID, IAM fetch runs once during start
+ - login() with an external ID fires a user state change
+
+ Expected:
+ - Exactly one IAM request total — the user state change does not produce an additional fetch
+ */
+ func testDoesNothingWhenNoRetryPending() throws {
+ /* Setup */
+ let client = MockOneSignalClient()
+ OneSignalCoreImpl.setSharedClient(client)
+ OneSignalInAppMessages.start()
+
+ MockUserRequests.setDefaultCreateAnonUserResponses(
+ with: client,
+ onesignalId: testOneSignalId,
+ subscriptionId: testPushSubId
+ )
+ ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId)
+ OneSignalUserManagerImpl.sharedInstance.start()
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
+
+ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestGetInAppMessages.self, expectedCount: 1))
+
+ /* Execute */
+ MockUserRequests.setDefaultIdentifyUserResponses(with: client, externalId: testExternalId)
+ OneSignalUserManagerImpl.sharedInstance.login(externalId: testExternalId, token: nil)
+ OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
+
+ /* Verify */
+ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestGetInAppMessages.self, expectedCount: 1))
+ }
+}
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h
new file mode 100644
index 000000000..813316635
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h
@@ -0,0 +1,20 @@
+//
+// Use this file to import your target's public headers that you would like to expose to Swift.
+//
+
+#import "OSInAppMessageInternal.h"
+#import "OSMessagingController.h"
+#import "OSInAppMessagingRequests.h"
+
+// Expose private properties and methods for testing
+@interface OSMessagingController (Testing)
+@property (nonatomic) BOOL hasCompletedFirstFetch;
+@property (strong, nonatomic, nonnull) NSMutableArray *messageDisplayQueue;
+@property (strong, nonatomic, nonnull) NSMutableSet *earlySessionTriggers;
+@property (strong, nonatomic, nonnull) NSMutableDictionary *redisplayedInAppMessages;
+@property (strong, nonatomic, nonnull) NSMutableArray *messages;
+@property (strong, nonatomic, nonnull) OSTriggerController *triggerController;
++ (void)start;
++ (void)removeInstance;
+- (void)presentInAppPreviewMessage:(OSInAppMessageInternal *)message;
+@end
diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/TriggerTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/TriggerTests.swift
new file mode 100644
index 000000000..cdc2aa7f0
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/TriggerTests.swift
@@ -0,0 +1,122 @@
+/*
+ Modified MIT License
+
+ Copyright 2025 OneSignal
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ 1. The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ 2. All copies of substantial portions of the Software may only be used in connection
+with services provided by OneSignal.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+import XCTest
+import OneSignalInAppMessagesMocks
+
+final class TriggerTests: XCTestCase {
+
+ override func setUpWithError() throws {
+ // Put setup code here. This method is called before the invocation of each test method in the class.
+ }
+
+ override func tearDownWithError() throws {
+ // Put teardown code here. This method is called after the invocation of each test method in the class.
+ }
+
+ /**
+ Test that NotEqualTo trigger does NOT match non-existent properties.
+ */
+ func testNotEqualToTrigger_doesNotMatchNonExistentProperty() throws {
+ /* Setup */
+ let triggerController = OSTriggerController()
+
+ // Create a message with NotEqualTo trigger
+ let messageJson = IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "prop",
+ triggerId: "test_trigger",
+ type: 3, // OSTriggerOperatorTypeNotEqualTo
+ value: "value"
+ )
+ let message = OSInAppMessageInternal.instance(withJson: messageJson)
+ XCTAssertNotNil(message, "Message should be created successfully")
+
+ // Test 1: NotEqualTo should NOT match when property doesn't exist
+ XCTAssertFalse(triggerController.messageMatchesTriggers(message!))
+
+ // Test 2: NotEqualTo SHOULD match when property exists with different value
+ triggerController.addTriggers(["prop": "other"])
+ XCTAssertTrue(triggerController.messageMatchesTriggers(message!))
+
+ // Test 3: NotEqualTo should NOT match when property exists with same value
+ triggerController.addTriggers(["prop": "value"])
+ XCTAssertFalse(triggerController.messageMatchesTriggers(message!))
+ }
+
+ /**
+ Test that NotExists trigger correctly matches non-existent properties.
+ */
+ func testNotExistsTrigger_matchesNonExistentProperty() throws {
+ /* Setup */
+ let triggerController = OSTriggerController()
+
+ // Create a message with NotExists trigger
+ let messageJson = IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "prop",
+ triggerId: "test_trigger",
+ type: 7, // OSTriggerOperatorTypeNotExists
+ value: "value"
+ )
+ let message = OSInAppMessageInternal.instance(withJson: messageJson)
+ XCTAssertNotNil(message, "Message should be created successfully")
+
+ // Test 1: NotExists SHOULD match when property doesn't exist
+ XCTAssertTrue(triggerController.messageMatchesTriggers(message!))
+
+ // Test 2: NotExists should NOT match when property exists
+ triggerController.addTriggers(["prop": "other"])
+ XCTAssertFalse(triggerController.messageMatchesTriggers(message!))
+ }
+
+ /**
+ Test that Exists trigger correctly matches existing properties.
+ */
+ func testExistsTrigger_matchesExistingProperty() throws {
+ /* Setup */
+ let triggerController = OSTriggerController()
+
+ // Create a message with Exists trigger
+ let messageJson = IAMTestHelpers.testMessageJsonWithTrigger(
+ kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
+ property: "prop",
+ triggerId: "test_trigger",
+ type: 6, // OSTriggerOperatorTypeExists
+ value: "value"
+ )
+ let message = OSInAppMessageInternal.instance(withJson: messageJson)
+ XCTAssertNotNil(message, "Message should be created successfully")
+
+ // Test 1: Exists should NOT match when property doesn't exist
+ XCTAssertFalse(triggerController.messageMatchesTriggers(message!))
+
+ // Test 2: Exists SHOULD match when property exists
+ triggerController.addTriggers(["prop": "other"])
+ XCTAssertTrue(triggerController.messageMatchesTriggers(message!))
+ }
+}
diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Executors/OSLiveActivitiesExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Executors/OSLiveActivitiesExecutor.swift
index decfe741b..23c7b3082 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Executors/OSLiveActivitiesExecutor.swift
+++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Executors/OSLiveActivitiesExecutor.swift
@@ -48,8 +48,9 @@ class RequestCache {
init(cacheKey: String, ttl: TimeInterval) {
self.cacheKey = cacheKey
self.ttl = ttl
- self.items = OneSignalUserDefaults.initShared()
- .getSavedCodeableData(forKey: cacheKey, defaultValue: nil) as? [String: OSLiveActivityRequest] ?? [String: OSLiveActivityRequest]()
+ let cached = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: cacheKey, defaultValue: nil)
+ // for safe-casting to the protocol, the intermediary cast to AnyObject is necessary
+ self.items = cached as? [String: AnyObject] as? [String: OSLiveActivityRequest] ?? [String: OSLiveActivityRequest]()
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignal.LiveActivities initialized token cache \(self): \(items)")
}
@@ -97,7 +98,7 @@ class RequestCache {
class UpdateRequestCache: RequestCache {
// An update token should not last longer than 8 hours, we keep for 24 hours to be safe.
- static let OneDayInSeconds = TimeInterval(60 * 60 * 24 * 365)
+ static let OneDayInSeconds = TimeInterval(60 * 60 * 24)
init() {
super.init(cacheKey: OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY, ttl: UpdateRequestCache.OneDayInSeconds)
@@ -113,10 +114,30 @@ class StartRequestCache: RequestCache {
}
}
+class ReceiveReceiptsRequestCache: RequestCache {
+ // Keep receive receipts requests for up to 30 days.
+ static let OneMonthInSeconds = TimeInterval(60 * 60 * 24 * 30)
+
+ init() {
+ super.init(cacheKey: OS_LIVE_ACTIVITIES_EXECUTOR_RECEIVE_RECEIPTS_KEY, ttl: ReceiveReceiptsRequestCache.OneMonthInSeconds)
+ }
+}
+
+class ClickedRequestCache: RequestCache {
+ // Keep click event requests for up to 30 days.
+ static let OneMonthInSeconds = TimeInterval(60 * 60 * 24 * 30)
+
+ init() {
+ super.init(cacheKey: OS_LIVE_ACTIVITIES_EXECUTOR_CLICKED_KEY, ttl: ClickedRequestCache.OneMonthInSeconds)
+ }
+}
+
class OSLiveActivitiesExecutor: OSPushSubscriptionObserver {
// The currently tracked update and start tokens (key) and their associated request (value). THESE ARE NOT THREAD SAFE
let updateTokens: UpdateRequestCache = UpdateRequestCache()
let startTokens: StartRequestCache = StartRequestCache()
+ let receiveReceipts: ReceiveReceiptsRequestCache = ReceiveReceiptsRequestCache()
+ let clickEvents: ClickedRequestCache = ClickedRequestCache()
// The live activities request dispatch queue, serial. This synchronizes access to `updateTokens` and `startTokens`.
private var requestDispatch: OSDispatchQueue
@@ -181,14 +202,20 @@ class OSLiveActivitiesExecutor: OSPushSubscriptionObserver {
private func caches(_ block: (RequestCache) -> Void) {
block(self.startTokens)
block(self.updateTokens)
+ block(self.receiveReceipts)
+ block(self.clickEvents)
}
private func getCache(_ request: OSLiveActivityRequest) -> RequestCache {
if request is OSLiveActivityUpdateTokenRequest {
return self.updateTokens
+ } else if request is OSLiveActivityStartTokenRequest {
+ return self.startTokens
+ } else if request is OSRequestLiveActivityClicked {
+ return self.clickEvents
}
- return self.startTokens
+ return self.receiveReceipts
}
private func executeRequest(_ cache: RequestCache, request: OSLiveActivityRequest) {
diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift
new file mode 100644
index 000000000..0d57ff67f
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/LiveActivityConstants.swift
@@ -0,0 +1,41 @@
+/*
+ Modified MIT License
+
+ Copyright 2025 OneSignal
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ 1. The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ 2. All copies of substantial portions of the Software may only be used in connection
+ with services provided by OneSignal.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+/// Constants used throughout the OneSignalLiveActivities module
+enum LiveActivityConstants {
+ /// URL components for OneSignal click tracking
+ enum Tracking {
+ static let scheme = "onesignal-liveactivity"
+ static let host = "track"
+ static let clickPath = "/click"
+ static let clickId = "clickId"
+ static let activityId = "activityId"
+ static let activityType = "activityType"
+ static let notificationId = "notificationId"
+ static let redirect = "redirect"
+ }
+}
diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift
new file mode 100644
index 000000000..aa5a6c485
--- /dev/null
+++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OSLiveActivityViewExtensions.swift
@@ -0,0 +1,149 @@
+/*
+ Modified MIT License
+
+ Copyright 2025 OneSignal
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ 1. The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ 2. All copies of substantial portions of the Software may only be used in connection
+ with services provided by OneSignal.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+// Effectively blanks out this file for Mac Catalyst
+#if targetEnvironment(macCatalyst)
+#else
+import WidgetKit
+import ActivityKit
+import SwiftUI
+
+@available(iOS 16.1, *)
+extension DynamicIsland {
+ /// Sets the URL that opens the corresponding app of a Live Activity when a user taps on the Live Activity.
+ /// Sets OneSignal activity metadata. See Important callout below on usage.
+ ///
+ /// By setting the URL with this function, it becomes the default URL for deep linking into the app
+ /// for each view of the Live Activity. However, if you include a
+ /// in the Live Activity,
+ /// the link takes priority over the default URL. When a person taps on the `Link`, it takes them to the
+ /// place in the app that corresponds to the URL of the `Link`.
+ ///
+ /// - Parameters:
+ /// - url: The URL that opens the app.
+ /// - context: The activity view context.
+ ///
+ /// - Returns: The configuration object for the Dynamic Island with the specified URL.
+ ///
+ /// > Important: Use instead of`.widgetURL`. Requires handling from your app's URL handling code
+ /// (e.g., `application(_:open:options:)` in AppDelegate or `onOpenURL` in SwiftUI) using the
+ /// `OneSignal.LiveActivities.trackClickAndReturnOriginal(url)` method.
+ public func onesignalWidgetURL(
+ _ url: URL?,
+ context: ActivityViewContext
+ ) -> DynamicIsland {
+ return self.widgetURL(LiveActivityTrackingUtils.generateTrackingDeepLink(originalURL: url, context: context))
+ }
+}
+
+@available(iOS 16.1, *)
+extension View {
+ /// Sets the URL to open in the containing app when the user clicks the widget.
+ /// Sets OneSignal activity metadata. See Important callout below on usage.
+ ///
+ /// - Parameters:
+ /// - url: The URL to open in the containing app.
+ /// - context: The activity view context.
+ /// - Returns: A view that opens the specified URL when the user clicks
+ /// the widget.
+ ///
+ /// Widgets support one `onesignalWidgetURL` modifier in their view hierarchy.
+ /// If multiple views have `onesignalWidgetURL` modifiers, the behavior is undefined.
+ ///
+ /// > Important: Use instead of`.widgetURL`. Requires handling from your app's URL handling code
+ /// (e.g., `application(_:open:options:)` in AppDelegate or `onOpenURL` in SwiftUI) using the
+ /// `OneSignal.LiveActivities.trackClickAndReturnOriginal(url)` method.
+ @MainActor @preconcurrency public func onesignalWidgetURL(_ url: URL?, context: ActivityViewContext) -> some View {
+ return self.widgetURL(LiveActivityTrackingUtils.generateTrackingDeepLink(originalURL: url, context: context))
+ }
+}
+
+// MARK: - Tracking Utilities
+
+/// Utilities for building and managing Live Activity tracking URLs
+enum LiveActivityTrackingUtils {
+ /// Generates a tracking deep link from an original URL and activity context
+ /// - Parameters:
+ /// - originalURL: The original URL to track clicks for
+ /// - context: The activity view context containing metadata
+ /// - Returns: The tracking URL, or nil if construction failed
+ @available(iOS 16.1, *)
+ static func generateTrackingDeepLink(originalURL: URL?, context: ActivityViewContext) -> URL? {
+ // Get activity metadata from context
+ let activityId = context.attributes.onesignal.activityId
+ let activityType = String(describing: T.self)
+ let notificationId = context.state.onesignal?.notificationId
+
+ return buildTrackingURL(
+ originalURL: originalURL,
+ activityId: activityId,
+ activityType: activityType,
+ notificationId: notificationId
+ )
+ }
+
+ /// Builds a tracking URL that wraps the original URL with OneSignal tracking parameters
+ /// - Parameters:
+ /// - originalURL: The original URL to track clicks for
+ /// - activityId: The activity identifier
+ /// - activityType: The activity type name
+ /// - notificationId: Optional notification ID
+ /// - Returns: The tracking URL, or nil if construction failed
+ static func buildTrackingURL(
+ originalURL: URL?,
+ activityId: String,
+ activityType: String,
+ notificationId: String?
+ ) -> URL? {
+ // Generate a unique click ID
+ let clickId = UUID().uuidString
+
+ // Build OneSignal tracking URL
+ var components = URLComponents()
+ components.scheme = LiveActivityConstants.Tracking.scheme
+ components.host = LiveActivityConstants.Tracking.host
+ components.path = LiveActivityConstants.Tracking.clickPath
+
+ var queryItems = [
+ URLQueryItem(name: LiveActivityConstants.Tracking.clickId, value: clickId),
+ URLQueryItem(name: LiveActivityConstants.Tracking.activityId, value: activityId),
+ URLQueryItem(name: LiveActivityConstants.Tracking.activityType, value: activityType),
+ URLQueryItem(name: LiveActivityConstants.Tracking.notificationId, value: notificationId)
+ ]
+
+ if let originalURL = originalURL {
+ // URLQueryItem automatically percent-encodes the value when URLComponents constructs the URL
+ // This ensures special characters like &, #, ?, etc. in the redirect URL are properly encoded
+ queryItems.append(URLQueryItem(name: LiveActivityConstants.Tracking.redirect, value: originalURL.absoluteString))
+ }
+
+ components.queryItems = queryItems
+
+ return components.url
+ }
+}
+#endif
diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift
index 95c381bc1..64ee3d0d1 100644
--- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift
+++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/OneSignalLiveActivitiesManagerImpl.swift
@@ -171,10 +171,60 @@ public class OneSignalLiveActivitiesManagerImpl: NSObject, OSLiveActivities {
contentState: contentState,
pushType: .token)
} catch let error {
- OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot start default live activity: " + error.localizedDescription)
+ OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot start default live activity: \(error)")
}
}
+ @objc
+ public static func trackClickAndReturnOriginal(_ url: URL) -> URL? {
+ // Check if this is a OneSignal click tracking URL
+ guard url.scheme == LiveActivityConstants.Tracking.scheme,
+ url.host == LiveActivityConstants.Tracking.host,
+ url.path == LiveActivityConstants.Tracking.clickPath,
+ let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
+ let queryItems = components.queryItems else
+ {
+ OneSignalLog.onesignalLog(.LL_VERBOSE, message: "trackClickAndReturnOriginal:\(url) is not a tracking URL")
+ return url
+ }
+
+ /// Helper function to extract redirect URL
+ func getRedirectURL() -> URL? {
+ guard let redirectString = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.redirect })?.value,
+ let redirectURL = URL(string: redirectString)
+ else {
+ return nil
+ }
+ return redirectURL
+ }
+
+ // Extract metadata
+ guard let clickId = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.clickId })?.value,
+ let activityId = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.activityId })?.value,
+ let activityType = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.activityType })?.value else
+ {
+ OneSignalLog.onesignalLog(.LL_ERROR, message: "Missing required parameters in tracking URL: \(url)")
+ return getRedirectURL()
+ }
+
+ let notificationId = queryItems.first(where: { $0.name == LiveActivityConstants.Tracking.notificationId })?.value
+
+ trackClick(clickId: clickId, activityType: activityType, activityId: activityId, notificationId: notificationId)
+
+ return getRedirectURL()
+ }
+
+ /**
+ Track the click event.
+ - Parameters:
+ - clickId: UUID representing the unique click event, as it is possible for this click to be tracked multiple times.
+ */
+ private static func trackClick(clickId: String, activityType: String, activityId: String, notificationId: String?) {
+ OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignal.LiveActivities trackClick called with clickId: \(clickId), activityType: \(activityType), activityId: \(activityId)")
+ let req = OSRequestLiveActivityClicked(key: clickId, activityType: activityType, activityId: activityId, notificationId: notificationId)
+ _executor.append(req)
+ }
+
@available(iOS 17.2, *)
private static func listenForPushToStart(_ activityType: Attributes.Type, options: LiveActivitySetupOptions? = nil) {
if options == nil || options!.enablePushToStart {
@@ -202,6 +252,9 @@ public class OneSignalLiveActivitiesManagerImpl: NSObject, OSLiveActivities {
for activity in Activity.activities {
listenForActivityStateUpdates(activityType, activity: activity, options: options)
listenForActivityPushToUpdate(activityType, activity: activity, options: options)
+ if #available(iOS 16.2, *) {
+ listenForContentUpdates(activityType, activity: activity)
+ }
}
// Establish listeners for activity updates
@@ -221,6 +274,9 @@ public class OneSignalLiveActivitiesManagerImpl: NSObject, OSLiveActivities {
listenForActivityStateUpdates(activityType, activity: activity, options: options)
listenForActivityPushToUpdate(activityType, activity: activity, options: options)
+ if #available(iOS 16.2, *) {
+ listenForContentUpdates(activityType, activity: activity)
+ }
}
}
}
@@ -272,5 +328,23 @@ public class OneSignalLiveActivitiesManagerImpl: NSObject, OSLiveActivities {
}
}
}
+
+ @available(iOS 16.2, *)
+ private static func listenForContentUpdates