diff --git a/.github/actions/setup-demo/action.yml b/.github/actions/setup-demo/action.yml index 5a725c8..75b6658 100644 --- a/.github/actions/setup-demo/action.yml +++ b/.github/actions/setup-demo/action.yml @@ -34,7 +34,6 @@ runs: echo "VITE_ONESIGNAL_APP_ID=${{ inputs.onesignal-app-id }}" > .env echo "VITE_ONESIGNAL_API_KEY=${{ inputs.onesignal-api-key }}" >> .env echo "VITE_ONESIGNAL_ANDROID_CHANNEL_ID=7ec2ece9-c538-4656-9516-1316f48a005c" >> .env - echo "VITE_E2E_MODE=true" >> .env - name: Install and set up demo shell: bash diff --git a/README.md b/README.md index fad6873..e0fb0dc 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ See the `examples/demo` directory for a full working example. -* [`initialize(...)`](#initialize) -* [`login(...)`](#login) -* [`logout()`](#logout) -* [`setConsentRequired(...)`](#setconsentrequired) -* [`setConsentGiven(...)`](#setconsentgiven) -* [Interfaces](#interfaces) -* [Type Aliases](#type-aliases) +- [`initialize(...)`](#initialize) +- [`login(...)`](#login) +- [`logout()`](#logout) +- [`setConsentRequired(...)`](#setconsentrequired) +- [`setConsentGiven(...)`](#setconsentgiven) +- [Interfaces](#interfaces) +- [Type Aliases](#type-aliases) @@ -51,8 +51,7 @@ Initialize the SDK with your OneSignal app ID. Call during app startup. | ----------- | ------------------- | | **`appId`** | string | --------------------- - +--- ### login(...) @@ -66,8 +65,7 @@ Log in to OneSignal as the user identified by `externalId`, switching the user c | ---------------- | ------------------- | | **`externalId`** | string | --------------------- - +--- ### logout() @@ -77,8 +75,7 @@ logout() => Promise Log out the current user. The SDK will reference a new device-scoped user. --------------------- - +--- ### setConsentRequired(...) @@ -92,8 +89,7 @@ Set whether user privacy consent is required before sending data to OneSignal. C | -------------- | -------------------- | | **`required`** | boolean | --------------------- - +--- ### setConsentGiven(...) @@ -107,12 +103,10 @@ Indicate whether the user has granted privacy consent. | ------------- | -------------------- | | **`granted`** | boolean | --------------------- - +--- ### Interfaces - #### OneSignalDebugAPI Debug helpers exposed via `OneSignal.Debug`. @@ -122,7 +116,6 @@ Debug helpers exposed via `OneSignal.Debug`. | **setLogLevel** | (logLevel: LogLevel) => void | Set the log level printed to LogCat (Android) or the Xcode console (iOS). | | **setAlertLevel** | (visualLogLevel: LogLevel) => void | Set the log level shown to the user as alert dialogs. | - #### OneSignalUserAPI Current-user operations exposed via `OneSignal.User`. @@ -153,14 +146,12 @@ Current-user operations exposed via `OneSignal.User`. | **getExternalId** | () => Promise<string \| null> | Get the external ID set via `login`, or null if the user is anonymous. | | **trackEvent** | (name: string, properties?: object \| undefined) => Promise<void> | Track a custom event with an optional set of JSON-serializable properties. | - #### UserChangedState | Prop | Type | | ------------- | ----------------------------------------------- | | **`current`** | UserState | - #### UserState | Prop | Type | @@ -168,7 +159,6 @@ Current-user operations exposed via `OneSignal.User`. | **`onesignalId`** | string | | **`externalId`** | string | - #### OneSignalPushSubscriptionAPI Push subscription state and controls exposed via `OneSignal.User.pushSubscription`. @@ -183,7 +173,6 @@ Push subscription state and controls exposed via `OneSignal.User.pushSubscriptio | **optIn** | () => Promise<void> | Opt the user in to push notifications. Prompts for permission if needed. | | **optOut** | () => Promise<void> | Opt the user out of push notifications on this device. | - #### PushSubscriptionChangedState | Prop | Type | @@ -191,7 +180,6 @@ Push subscription state and controls exposed via `OneSignal.User.pushSubscriptio | **`previous`** | PushSubscriptionState | | **`current`** | PushSubscriptionState | - #### PushSubscriptionState | Prop | Type | @@ -200,7 +188,6 @@ Push subscription state and controls exposed via `OneSignal.User.pushSubscriptio | **`token`** | string | | **`optedIn`** | boolean | - #### OneSignalNotificationsAPI Notification permission and event handling exposed via `OneSignal.Notifications`. @@ -218,7 +205,6 @@ Notification permission and event handling exposed via `OneSignal.Notifications` | **removeNotification** | (id: number) => Promise<void> | Android only. Cancel a single notification by its Android notification ID. | | **removeGroupedNotifications** | (id: string) => Promise<void> | Android only. Cancel a group of notifications by group key. | - #### NotificationClickEvent | Prop | Type | @@ -226,7 +212,6 @@ Notification permission and event handling exposed via `OneSignal.Notifications` | **`result`** | NotificationClickResult | | **`notification`** | OSNotification | - #### NotificationClickResult | Prop | Type | @@ -234,7 +219,6 @@ Notification permission and event handling exposed via `OneSignal.Notifications` | **`actionId`** | string | | **`url`** | string | - #### OneSignalInAppMessagesAPI In-app message triggers and event handling exposed via `OneSignal.InAppMessages`. @@ -251,7 +235,6 @@ In-app message triggers and event handling exposed via `OneSignal.InAppMessages` | **setPaused** | (pause: boolean) => void | Pause or resume the display of in-app messages. | | **getPaused** | () => Promise<boolean> | Whether in-app messaging is currently paused. | - #### InAppMessageClickEvent | Prop | Type | @@ -259,14 +242,12 @@ In-app message triggers and event handling exposed via `OneSignal.InAppMessages` | **`message`** | OSInAppMessage | | **`result`** | InAppMessageClickResult | - #### OSInAppMessage | Prop | Type | | --------------- | ------------------- | | **`messageId`** | string | - #### InAppMessageClickResult | Prop | Type | @@ -276,35 +257,30 @@ In-app message triggers and event handling exposed via `OneSignal.InAppMessages` | **`url`** | string | | **`urlTarget`** | InAppMessageActionUrlType | - #### InAppMessageWillDisplayEvent | Prop | Type | | ------------- | --------------------------------------------------------- | | **`message`** | OSInAppMessage | - #### InAppMessageDidDisplayEvent | Prop | Type | | ------------- | --------------------------------------------------------- | | **`message`** | OSInAppMessage | - #### InAppMessageWillDismissEvent | Prop | Type | | ------------- | --------------------------------------------------------- | | **`message`** | OSInAppMessage | - #### InAppMessageDidDismissEvent | Prop | Type | | ------------- | --------------------------------------------------------- | | **`message`** | OSInAppMessage | - #### OneSignalSessionAPI Outcome reporting exposed via `OneSignal.Session`. @@ -315,7 +291,6 @@ Outcome reporting exposed via `OneSignal.Session`. | **addUniqueOutcome** | (name: string) => Promise<void> | Record a unique outcome with the given name against the current session. | | **addOutcomeWithValue** | (name: string, value: number) => Promise<void> | Record an outcome with the given name and value against the current session. | - #### OneSignalLocationAPI Location permission and sharing exposed via `OneSignal.Location`. @@ -326,7 +301,6 @@ Location permission and sharing exposed via `OneSignal.Location`. | **setShared** | (shared: boolean) => void | Enable or disable sharing the device location with OneSignal. | | **isShared** | () => Promise<boolean> | Whether the device location is currently shared with OneSignal. | - #### OneSignalLiveActivitiesAPI Live activity controls exposed via `OneSignal.LiveActivities`. iOS only unless noted. @@ -340,56 +314,48 @@ Live activity controls exposed via `OneSignal.LiveActivities`. iOS only unless n | **setupDefault** | (options?: LiveActivitySetupOptions \| undefined) => Promise<void> | Set up the OneSignal default live activity, optionally enabling pushToStart/pushToUpdate. | | **startDefault** | (activityId: string, attributes: Record<string, unknown>, content: Record<string, unknown>) => Promise<void> | Start a live activity backed by the OneSignal default attributes type. | - ### Type Aliases - #### LogLevel (typeof LogLevel)[keyof typeof LogLevel] - #### Record Construct a type with a set of properties K of type T -{ [P in K]: T; } - +{ +[P in K]: T; +} #### OSNotificationPermission (typeof OSNotificationPermission)[keyof typeof OSNotificationPermission] - #### NotificationEventName 'click' | 'foregroundWillDisplay' | 'permissionChange' - #### NotificationEventTypeMap { click: NotificationClickEvent; foregroundWillDisplay: NotificationWillDisplayEvent; permissionChange: boolean; } - #### InAppMessageEventName 'click' | 'willDisplay' | 'didDisplay' | 'willDismiss' | 'didDismiss' - #### InAppMessageEventTypeMap { click: InAppMessageClickEvent; willDisplay: InAppMessageWillDisplayEvent; didDisplay: InAppMessageDidDisplayEvent; willDismiss: InAppMessageWillDismissEvent; didDismiss: InAppMessageDidDismissEvent; } - #### InAppMessageActionUrlType 'browser' | 'webview' | 'replacement' - #### LiveActivitySetupOptions The setup options for `OneSignal.LiveActivities.setupDefault`. -{ /** * When true, OneSignal will listen for pushToStart tokens for the `OneSignalLiveActivityAttributes` structure. */ enablePushToStart: boolean; /** * When true, OneSignal will listen for pushToUpdate tokens for each start live activity that uses the * `OneSignalLiveActivityAttributes` structure. */ enablePushToUpdate: boolean; } +{ /** _ When true, OneSignal will listen for pushToStart tokens for the `OneSignalLiveActivityAttributes` structure. _/ enablePushToStart: boolean; /** _ When true, OneSignal will listen for pushToUpdate tokens for each start live activity that uses the _ `OneSignalLiveActivityAttributes` structure. \*/ enablePushToUpdate: boolean; } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 85fa1be..fbbe8ab 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -25,7 +25,7 @@ compileSdk = "36" junit = "4.13.2" kotlin = "2.2.20" minSdk = "24" -onesignal = "5.9.1" +onesignal = "5.9.2" targetSdk = "36" [libraries] diff --git a/android/src/main/kotlin/com/onesignal/capacitor/OneSignalCapacitorPlugin.kt b/android/src/main/kotlin/com/onesignal/capacitor/OneSignalCapacitorPlugin.kt index 928c98a..9d8ea8a 100644 --- a/android/src/main/kotlin/com/onesignal/capacitor/OneSignalCapacitorPlugin.kt +++ b/android/src/main/kotlin/com/onesignal/capacitor/OneSignalCapacitorPlugin.kt @@ -134,7 +134,7 @@ class OneSignalCapacitorPlugin : Plugin(), initialized = true OneSignalWrapper.sdkType = "capacitor" - OneSignalWrapper.sdkVersion = "010004" + OneSignalWrapper.sdkVersion = "010005" OneSignal.initWithContext(context, appId) // If the SDK was initialized from a non-Activity context (FCM/work diff --git a/bun.lock b/bun.lock index 9e541da..bd86ab5 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,6 @@ "@capacitor/docgen": "^0.2.2", "@types/bun": "latest", "@vitest/coverage-v8": "^4.1.2", - "happy-dom": "^20.9.0", "typescript": "^5.9.3", "vite-plus": "0.1.20", }, @@ -149,10 +148,6 @@ "@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="], - "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], - - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.5", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.5", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.5", "vitest": "4.1.5" }, "optionalPeers": ["@vitest/browser"] }, "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A=="], "@vitest/pretty-format": ["@vitest/pretty-format@4.1.5", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g=="], @@ -191,8 +186,6 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], @@ -203,8 +196,6 @@ "github-slugger": ["github-slugger@1.5.0", "", {}, "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw=="], - "happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], @@ -303,16 +294,10 @@ "vitest": ["@voidzero-dev/vite-plus-test@0.1.20", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@voidzero-dev/vite-plus-core": "0.1.20", "es-module-lexer": "^1.7.0", "obug": "^2.1.1", "pixelmatch": "^7.1.0", "pngjs": "^7.0.0", "sirv": "^3.0.2", "std-env": "^4.0.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "ws": "^8.18.3" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/coverage-istanbul": "4.1.5", "@vitest/coverage-v8": "4.1.5", "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"] }, "sha512-vy2dJYw1bhgQ/+BrQrfwPlSKzQ2mm3YLJ9kGF7Yo0UJ2P3XKpshtgFIWLjSg/IASnC93OAx0c/7j3NM0I1RMuA=="], - "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], - "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], "@capacitor/docgen/typescript": ["typescript@4.2.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg=="], - "@types/ws/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - "bun-types/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], - - "happy-dom/@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], } } diff --git a/examples/build.md b/examples/build.md index 0461b15..b59aa2e 100644 --- a/examples/build.md +++ b/examples/build.md @@ -274,10 +274,8 @@ examples/ │ │ ├── CustomEventsSection.tsx │ │ ├── LocationSection.tsx │ │ └── LiveActivitySection.tsx - │ ├── theme/ - │ │ └── variables.css - │ └── utils/ - │ └── maskValue.ts # E2E_MODE bullet masking helper + │ └── theme/ + │ └── variables.css ├── android/ # Capacitor Android project └── ios/ └── App/ diff --git a/examples/demo/.env.example b/examples/demo/.env.example index 60c2d3e..bdc9292 100644 --- a/examples/demo/.env.example +++ b/examples/demo/.env.example @@ -1,4 +1,3 @@ VITE_ONESIGNAL_APP_ID=your_onesignal_app_id VITE_ONESIGNAL_API_KEY=your_rest_api_key -VITE_ONESIGNAL_ANDROID_CHANNEL_ID= -VITE_E2E_MODE=false \ No newline at end of file +VITE_ONESIGNAL_ANDROID_CHANNEL_ID= \ No newline at end of file diff --git a/examples/demo/README.md b/examples/demo/README.md index 80edb7e..45a299f 100644 --- a/examples/demo/README.md +++ b/examples/demo/README.md @@ -31,7 +31,6 @@ cp .env.example .env VITE_ONESIGNAL_APP_ID= VITE_ONESIGNAL_API_KEY= VITE_ONESIGNAL_ANDROID_CHANNEL_ID= -VITE_E2E_MODE=false ``` --- diff --git a/examples/demo/android/app/src/main/res/values/styles.xml b/examples/demo/android/app/src/main/res/values/styles.xml index be874e5..59cf157 100644 --- a/examples/demo/android/app/src/main/res/values/styles.xml +++ b/examples/demo/android/app/src/main/res/values/styles.xml @@ -17,6 +17,8 @@ \ No newline at end of file diff --git a/examples/demo/src/components/sections/AppSection.tsx b/examples/demo/src/components/sections/AppSection.tsx index cb04999..72f2fe5 100644 --- a/examples/demo/src/components/sections/AppSection.tsx +++ b/examples/demo/src/components/sections/AppSection.tsx @@ -1,7 +1,6 @@ import { IonToggle } from '@ionic/react'; import type { FC } from 'react'; -import { maskValue } from '../../utils/maskValue'; import SectionCard from '../SectionCard'; interface AppSectionProps { @@ -24,7 +23,7 @@ const AppSection: FC = ({
App ID - {maskValue(appId)} + {appId}
diff --git a/examples/demo/src/components/sections/PushSection.tsx b/examples/demo/src/components/sections/PushSection.tsx index 540d1c5..c43545d 100644 --- a/examples/demo/src/components/sections/PushSection.tsx +++ b/examples/demo/src/components/sections/PushSection.tsx @@ -1,7 +1,6 @@ import { IonToggle } from '@ionic/react'; import type { FC } from 'react'; -import { maskValue } from '../../utils/maskValue'; import ActionButton from '../ActionButton'; import SectionCard from '../SectionCard'; @@ -27,7 +26,7 @@ const PushSection: FC = ({
Push ID - {maskValue(pushSubscriptionId ?? '—')} + {pushSubscriptionId ?? '—'}
diff --git a/examples/demo/src/components/sections/UserSection.tsx b/examples/demo/src/components/sections/UserSection.tsx index ddaee1f..56ea78a 100644 --- a/examples/demo/src/components/sections/UserSection.tsx +++ b/examples/demo/src/components/sections/UserSection.tsx @@ -25,7 +25,7 @@ const UserSection: FC = ({ externalUserId, onLogin, onLogout }
External ID - {externalUserId ?? '–'} + {externalUserId ?? '—'}
diff --git a/examples/demo/src/hooks/useOneSignal.ts b/examples/demo/src/hooks/useOneSignal.ts index ad3e5bd..953af0d 100644 --- a/examples/demo/src/hooks/useOneSignal.ts +++ b/examples/demo/src/hooks/useOneSignal.ts @@ -18,6 +18,18 @@ const RESOLVED_APP_ID = APP_ID?.trim() || DEFAULT_APP_ID; const apiService = OneSignalApiService.getInstance(); const preferences = PreferencesService.getInstance(); +// uncomment to debug ios logs in safari web inspector +// const buf: string[] = []; +// (['log', 'warn', 'error'] as const).forEach((level) => { +// const orig = console[level].bind(console); +// console[level] = (...args) => { +// buf.push(`[${level}] ${args.map(String).join(' ')}`); +// localStorage.setItem('__logs', JSON.stringify(buf.slice(-500))); +// orig(...args); +// }; +// }); +// then later call JSON.parse(localStorage.getItem('__logs')).forEach(l => console.log(l)) + // One-shot SDK initialization at module-eval time. Capacitor's bridge queues // calls until native is ready, so no `deviceready` gating is required. The // downstream `OneSignal.initialize` short-circuits on the native side, but @@ -47,8 +59,6 @@ function initOneSignal(): void { if (storedExternalUserId) { void OneSignal.login(storedExternalUserId); } - - console.log(`OneSignal initialized with app ID: ${RESOLVED_APP_ID}`); } initOneSignal(); @@ -79,6 +89,7 @@ export type UseOneSignalReturn = { consentRequired: boolean; privacyConsentGiven: boolean; externalUserId: string | undefined; + oneSignalId: string | undefined; pushSubscriptionId: string | undefined; isPushEnabled: boolean; hasNotificationPermission: boolean; @@ -139,6 +150,7 @@ export function useOneSignal(): UseOneSignalReturn { preferences.getConsentGiven(), ); const [externalUserId, setExternalUserId] = useState(undefined); + const [oneSignalId, setOneSignalId] = useState(undefined); const [pushSubscriptionId, setPushSubscriptionId] = useState(undefined); const [isPushEnabled, setIsPushEnabled] = useState(false); const [hasNotificationPermission, setHasNotificationPermission] = useState(false); @@ -196,22 +208,6 @@ export function useOneSignal(): UseOneSignalReturn { const handleNotificationClick = (e: NotificationClickEvent) => { console.log(`Notification click: ${e.notification.title ?? ''}`); - // Persist to localStorage so cold-start clicks are still inspectable - // after the Safari Web Inspector reattaches to the WKWebView. - try { - const existing = JSON.parse(localStorage.getItem('lastNotificationClicks') ?? '[]'); - existing.push({ - notificationId: e.notification.notificationId, - title: e.notification.title ?? null, - body: e.notification.body ?? null, - actionId: e.result.actionId ?? null, - url: e.result.url ?? null, - receivedAt: new Date().toISOString(), - }); - localStorage.setItem('lastNotificationClicks', JSON.stringify(existing.slice(-20))); - } catch (err) { - console.warn('Failed to persist notification click to localStorage', err); - } }; const handleForegroundWillDisplay = (e: NotificationWillDisplayEvent) => { @@ -245,6 +241,8 @@ export function useOneSignal(): UseOneSignalReturn { `User changed: onesignalId=${nextOnesignalId ?? 'null'}, externalId=${event.current.externalId ?? 'null'}`, ); + setOneSignalId(nextOnesignalId ?? undefined); + if (nextOnesignalId === null) return; void fetchUserDataFromApi(); }; @@ -261,11 +259,6 @@ export function useOneSignal(): UseOneSignalReturn { OneSignal.User.addEventListener('change', userChangeHandler); const load = async () => { - // Uncomment if you want so you have time to see logs while trying to open - // safari web inspector. Not an issue for chrome web inspector. - // await new Promise((resolve) => setTimeout(resolve, 10_000)); - // if (cancelled) return; - const [externalId, pushId, pushOptedIn, hasPerm, initialOnesignalId] = await Promise.all([ OneSignal.User.getExternalId(), OneSignal.User.pushSubscription.getIdAsync(), @@ -276,6 +269,7 @@ export function useOneSignal(): UseOneSignalReturn { if (cancelled) return; setExternalUserId(externalId ?? preferences.getExternalUserId() ?? undefined); + setOneSignalId(initialOnesignalId ?? undefined); setPushSubscriptionId(pushId ?? undefined); setIsPushEnabled(pushOptedIn); setHasNotificationPermission(hasPerm); @@ -543,6 +537,7 @@ export function useOneSignal(): UseOneSignalReturn { consentRequired, privacyConsentGiven, externalUserId, + oneSignalId, pushSubscriptionId, isPushEnabled, hasNotificationPermission, diff --git a/examples/demo/src/pages/HomeScreen.css b/examples/demo/src/pages/HomeScreen.css index 11e0f47..01bd9ce 100644 --- a/examples/demo/src/pages/HomeScreen.css +++ b/examples/demo/src/pages/HomeScreen.css @@ -41,7 +41,7 @@ .brand-header { background: var(--os-primary); - border-bottom: 1px solid #fff; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); color: #fff; min-height: var(--demo-header-height); padding-top: var(--ion-safe-area-top); @@ -150,6 +150,8 @@ .kv-card .kv-row .id-value { font-size: var(--font-size-body-small); font-family: monospace; + user-select: all; + -webkit-user-select: all; } .divider { diff --git a/examples/demo/src/services/OneSignalApiService.ts b/examples/demo/src/services/OneSignalApiService.ts index 3baa348..0b515e6 100644 --- a/examples/demo/src/services/OneSignalApiService.ts +++ b/examples/demo/src/services/OneSignalApiService.ts @@ -74,34 +74,56 @@ class OneSignalApiService { subscriptionId: string, extra: Record, ): Promise { - try { - const body = { - app_id: this.appId, - include_subscription_ids: [subscriptionId], - headings, - contents, - ...extra, - }; - - const response = await CapacitorHttp.post({ - url: 'https://onesignal.com/api/v1/notifications', - headers: { - Accept: 'application/vnd.onesignal.v1+json', - 'Content-Type': 'application/json', - }, - data: body, - }); - - if (response.status < 200 || response.status >= 300) { - console.error(`Send notification failed: ${JSON.stringify(response.data)}`); + const body = { + app_id: this.appId, + include_subscription_ids: [subscriptionId], + headings, + contents, + ...extra, + }; + + const maxAttempts = 5; + const backoffMs = (n: number) => 2_000 * 2 ** (n - 1); + + // Retry on `invalid_player_ids` to absorb the brief race where the + // subscription has been created locally but is not yet visible to the + // /notifications endpoint. + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const response = await CapacitorHttp.post({ + url: 'https://onesignal.com/api/v1/notifications', + headers: { + Accept: 'application/vnd.onesignal.v1+json', + 'Content-Type': 'application/json', + }, + data: body, + }); + + if (response.status < 200 || response.status >= 300) { + console.error(`Send notification failed: ${JSON.stringify(response.data)}`); + return false; + } + + const invalidIds = response.data?.errors?.invalid_player_ids; + if (Array.isArray(invalidIds) && invalidIds.length > 0) { + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, backoffMs(attempt))); + continue; + } + console.error( + `Send notification failed: invalid_player_ids ${JSON.stringify(invalidIds)}`, + ); + return false; + } + + return true; + } catch (err) { + console.error(`Send notification error: ${String(err)}`); return false; } - - return true; - } catch (err) { - console.error(`Send notification error: ${String(err)}`); - return false; } + + return false; } async updateLiveActivity( diff --git a/examples/demo/src/utils/maskValue.ts b/examples/demo/src/utils/maskValue.ts deleted file mode 100644 index 12d1582..0000000 --- a/examples/demo/src/utils/maskValue.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Mirrors the trim/strict-equals check used elsewhere so a stray newline -// or whitespace in `.env` doesn't accidentally enable masking. -const E2E_MODE = (import.meta.env.VITE_E2E_MODE ?? '').trim() === 'true'; -const MASK_CHAR = '•'; - -export function maskValue(value: string): string { - if (E2E_MODE) { - return MASK_CHAR.repeat(value.length); - } - return value; -} diff --git a/examples/demo_cap7/src/app/app.component.ts b/examples/demo_cap7/src/app/app.component.ts index e6ce849..d32ae82 100644 --- a/examples/demo_cap7/src/app/app.component.ts +++ b/examples/demo_cap7/src/app/app.component.ts @@ -90,26 +90,42 @@ export class AppComponent { return; } - const response = await CapacitorHttp.post({ - url: 'https://onesignal.com/api/v1/notifications', - headers: { - Accept: 'application/vnd.onesignal.v1+json', - 'Content-Type': 'application/json', - }, - data: { - app_id: ONESIGNAL_APP_ID, - include_subscription_ids: [subscriptionId], - headings: { en: 'Simple Notification' }, - contents: { en: 'This is a simple push notification' }, - }, - }); - - if (response.status < 200 || response.status >= 300) { - this.log(`Send failed (${response.status}): ${JSON.stringify(response.data)}`); + const body = { + app_id: ONESIGNAL_APP_ID, + include_subscription_ids: [subscriptionId], + headings: { en: 'Simple Notification' }, + contents: { en: 'This is a simple push notification' }, + }; + const maxAttempts = 3; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + const response = await CapacitorHttp.post({ + url: 'https://onesignal.com/api/v1/notifications', + headers: { + Accept: 'application/vnd.onesignal.v1+json', + 'Content-Type': 'application/json', + }, + data: body, + }); + + if (response.status < 200 || response.status >= 300) { + this.log(`Send failed (${response.status}): ${JSON.stringify(response.data)}`); + return; + } + + const invalidIds = response.data?.errors?.invalid_player_ids; + if (Array.isArray(invalidIds) && invalidIds.length > 0) { + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, 3_000 * attempt)); + continue; + } + this.log(`Send failed: invalid_player_ids ${JSON.stringify(invalidIds)}`); + return; + } + + this.log(`Sent. response: ${JSON.stringify(response.data)}`); return; } - - this.log(`Sent. response: ${JSON.stringify(response.data)}`); } catch (err) { this.log(`sendTestNotification error: ${String(err)}`); } finally { diff --git a/examples/demo_pods/.env.example b/examples/demo_pods/.env.example index a9c529a..b15cf8d 100644 --- a/examples/demo_pods/.env.example +++ b/examples/demo_pods/.env.example @@ -1,4 +1,3 @@ VITE_ONESIGNAL_APP_ID=your_onesignal_app_id VITE_ONESIGNAL_API_KEY=your_rest_api_key VITE_ONESIGNAL_ANDROID_CHANNEL_ID= -VITE_E2E_MODE=false diff --git a/examples/demo_pods/android/app/src/main/res/values/styles.xml b/examples/demo_pods/android/app/src/main/res/values/styles.xml index be874e5..59cf157 100644 --- a/examples/demo_pods/android/app/src/main/res/values/styles.xml +++ b/examples/demo_pods/android/app/src/main/res/values/styles.xml @@ -17,6 +17,8 @@ \ No newline at end of file diff --git a/examples/demo_pods/capacitor.config.ts b/examples/demo_pods/capacitor.config.ts index 07dfa5f..3b26df2 100644 --- a/examples/demo_pods/capacitor.config.ts +++ b/examples/demo_pods/capacitor.config.ts @@ -13,6 +13,11 @@ const config: CapacitorConfig = { // the WebView. Test-only convenience for the demo app. webContentsDebuggingEnabled: true, }, + plugins: { + SplashScreen: { + backgroundColor: '#ffffff', + }, + }, }; export default config; diff --git a/examples/demo_pods/src/components/sections/AppSection.tsx b/examples/demo_pods/src/components/sections/AppSection.tsx index cb04999..72f2fe5 100644 --- a/examples/demo_pods/src/components/sections/AppSection.tsx +++ b/examples/demo_pods/src/components/sections/AppSection.tsx @@ -1,7 +1,6 @@ import { IonToggle } from '@ionic/react'; import type { FC } from 'react'; -import { maskValue } from '../../utils/maskValue'; import SectionCard from '../SectionCard'; interface AppSectionProps { @@ -24,7 +23,7 @@ const AppSection: FC = ({
App ID - {maskValue(appId)} + {appId}
diff --git a/examples/demo_pods/src/components/sections/PushSection.tsx b/examples/demo_pods/src/components/sections/PushSection.tsx index 540d1c5..c43545d 100644 --- a/examples/demo_pods/src/components/sections/PushSection.tsx +++ b/examples/demo_pods/src/components/sections/PushSection.tsx @@ -1,7 +1,6 @@ import { IonToggle } from '@ionic/react'; import type { FC } from 'react'; -import { maskValue } from '../../utils/maskValue'; import ActionButton from '../ActionButton'; import SectionCard from '../SectionCard'; @@ -27,7 +26,7 @@ const PushSection: FC = ({
Push ID - {maskValue(pushSubscriptionId ?? '—')} + {pushSubscriptionId ?? '—'}
diff --git a/examples/demo_pods/src/components/sections/UserSection.tsx b/examples/demo_pods/src/components/sections/UserSection.tsx index ddaee1f..56ea78a 100644 --- a/examples/demo_pods/src/components/sections/UserSection.tsx +++ b/examples/demo_pods/src/components/sections/UserSection.tsx @@ -25,7 +25,7 @@ const UserSection: FC = ({ externalUserId, onLogin, onLogout }
External ID - {externalUserId ?? '–'} + {externalUserId ?? '—'}
diff --git a/examples/demo_pods/src/hooks/useOneSignal.ts b/examples/demo_pods/src/hooks/useOneSignal.ts index f06e897..953af0d 100644 --- a/examples/demo_pods/src/hooks/useOneSignal.ts +++ b/examples/demo_pods/src/hooks/useOneSignal.ts @@ -18,6 +18,18 @@ const RESOLVED_APP_ID = APP_ID?.trim() || DEFAULT_APP_ID; const apiService = OneSignalApiService.getInstance(); const preferences = PreferencesService.getInstance(); +// uncomment to debug ios logs in safari web inspector +// const buf: string[] = []; +// (['log', 'warn', 'error'] as const).forEach((level) => { +// const orig = console[level].bind(console); +// console[level] = (...args) => { +// buf.push(`[${level}] ${args.map(String).join(' ')}`); +// localStorage.setItem('__logs', JSON.stringify(buf.slice(-500))); +// orig(...args); +// }; +// }); +// then later call JSON.parse(localStorage.getItem('__logs')).forEach(l => console.log(l)) + // One-shot SDK initialization at module-eval time. Capacitor's bridge queues // calls until native is ready, so no `deviceready` gating is required. The // downstream `OneSignal.initialize` short-circuits on the native side, but @@ -47,8 +59,6 @@ function initOneSignal(): void { if (storedExternalUserId) { void OneSignal.login(storedExternalUserId); } - - console.log(`OneSignal initialized with app ID: ${RESOLVED_APP_ID}`); } initOneSignal(); @@ -79,6 +89,7 @@ export type UseOneSignalReturn = { consentRequired: boolean; privacyConsentGiven: boolean; externalUserId: string | undefined; + oneSignalId: string | undefined; pushSubscriptionId: string | undefined; isPushEnabled: boolean; hasNotificationPermission: boolean; @@ -139,6 +150,7 @@ export function useOneSignal(): UseOneSignalReturn { preferences.getConsentGiven(), ); const [externalUserId, setExternalUserId] = useState(undefined); + const [oneSignalId, setOneSignalId] = useState(undefined); const [pushSubscriptionId, setPushSubscriptionId] = useState(undefined); const [isPushEnabled, setIsPushEnabled] = useState(false); const [hasNotificationPermission, setHasNotificationPermission] = useState(false); @@ -196,27 +208,16 @@ export function useOneSignal(): UseOneSignalReturn { const handleNotificationClick = (e: NotificationClickEvent) => { console.log(`Notification click: ${e.notification.title ?? ''}`); - // Persist to localStorage so cold-start clicks are still inspectable - // after the Safari Web Inspector reattaches to the WKWebView. - try { - const existing = JSON.parse(localStorage.getItem('lastNotificationClicks') ?? '[]'); - existing.push({ - notificationId: e.notification.notificationId, - title: e.notification.title ?? null, - body: e.notification.body ?? null, - actionId: e.result.actionId ?? null, - url: e.result.url ?? null, - receivedAt: new Date().toISOString(), - }); - localStorage.setItem('lastNotificationClicks', JSON.stringify(existing.slice(-20))); - } catch (err) { - console.warn('Failed to persist notification click to localStorage', err); - } }; const handleForegroundWillDisplay = (e: NotificationWillDisplayEvent) => { console.log(`Notification foregroundWillDisplay: ${e.getNotification().title ?? ''}`); - e.getNotification().display(); + + // uncomment to test preventing the default display behavior + // e.preventDefault(); + + // can call this after preventDefault to force display of notification + // e.getNotification().display(); }; const pushSubHandler = (event: PushSubscriptionChangedState) => { @@ -230,6 +231,7 @@ export function useOneSignal(): UseOneSignalReturn { }; const permissionHandler = (granted: boolean) => { + console.log(`Permission changed: ${granted}`); setHasNotificationPermission(granted); }; @@ -239,6 +241,8 @@ export function useOneSignal(): UseOneSignalReturn { `User changed: onesignalId=${nextOnesignalId ?? 'null'}, externalId=${event.current.externalId ?? 'null'}`, ); + setOneSignalId(nextOnesignalId ?? undefined); + if (nextOnesignalId === null) return; void fetchUserDataFromApi(); }; @@ -255,11 +259,6 @@ export function useOneSignal(): UseOneSignalReturn { OneSignal.User.addEventListener('change', userChangeHandler); const load = async () => { - // Uncomment if you want so you have time to see logs while trying to open - // safari web inspector. Not an issue for chrome web inspector. - // await new Promise((resolve) => setTimeout(resolve, 10_000)); - // if (cancelled) return; - const [externalId, pushId, pushOptedIn, hasPerm, initialOnesignalId] = await Promise.all([ OneSignal.User.getExternalId(), OneSignal.User.pushSubscription.getIdAsync(), @@ -270,6 +269,7 @@ export function useOneSignal(): UseOneSignalReturn { if (cancelled) return; setExternalUserId(externalId ?? preferences.getExternalUserId() ?? undefined); + setOneSignalId(initialOnesignalId ?? undefined); setPushSubscriptionId(pushId ?? undefined); setIsPushEnabled(pushOptedIn); setHasNotificationPermission(hasPerm); @@ -537,6 +537,7 @@ export function useOneSignal(): UseOneSignalReturn { consentRequired, privacyConsentGiven, externalUserId, + oneSignalId, pushSubscriptionId, isPushEnabled, hasNotificationPermission, diff --git a/examples/demo_pods/src/pages/HomeScreen.css b/examples/demo_pods/src/pages/HomeScreen.css index 11e0f47..27485e5 100644 --- a/examples/demo_pods/src/pages/HomeScreen.css +++ b/examples/demo_pods/src/pages/HomeScreen.css @@ -41,7 +41,7 @@ .brand-header { background: var(--os-primary); - border-bottom: 1px solid #fff; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); color: #fff; min-height: var(--demo-header-height); padding-top: var(--ion-safe-area-top); diff --git a/examples/demo_pods/src/services/OneSignalApiService.ts b/examples/demo_pods/src/services/OneSignalApiService.ts index 3baa348..0b515e6 100644 --- a/examples/demo_pods/src/services/OneSignalApiService.ts +++ b/examples/demo_pods/src/services/OneSignalApiService.ts @@ -74,34 +74,56 @@ class OneSignalApiService { subscriptionId: string, extra: Record, ): Promise { - try { - const body = { - app_id: this.appId, - include_subscription_ids: [subscriptionId], - headings, - contents, - ...extra, - }; - - const response = await CapacitorHttp.post({ - url: 'https://onesignal.com/api/v1/notifications', - headers: { - Accept: 'application/vnd.onesignal.v1+json', - 'Content-Type': 'application/json', - }, - data: body, - }); - - if (response.status < 200 || response.status >= 300) { - console.error(`Send notification failed: ${JSON.stringify(response.data)}`); + const body = { + app_id: this.appId, + include_subscription_ids: [subscriptionId], + headings, + contents, + ...extra, + }; + + const maxAttempts = 5; + const backoffMs = (n: number) => 2_000 * 2 ** (n - 1); + + // Retry on `invalid_player_ids` to absorb the brief race where the + // subscription has been created locally but is not yet visible to the + // /notifications endpoint. + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const response = await CapacitorHttp.post({ + url: 'https://onesignal.com/api/v1/notifications', + headers: { + Accept: 'application/vnd.onesignal.v1+json', + 'Content-Type': 'application/json', + }, + data: body, + }); + + if (response.status < 200 || response.status >= 300) { + console.error(`Send notification failed: ${JSON.stringify(response.data)}`); + return false; + } + + const invalidIds = response.data?.errors?.invalid_player_ids; + if (Array.isArray(invalidIds) && invalidIds.length > 0) { + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, backoffMs(attempt))); + continue; + } + console.error( + `Send notification failed: invalid_player_ids ${JSON.stringify(invalidIds)}`, + ); + return false; + } + + return true; + } catch (err) { + console.error(`Send notification error: ${String(err)}`); return false; } - - return true; - } catch (err) { - console.error(`Send notification error: ${String(err)}`); - return false; } + + return false; } async updateLiveActivity( diff --git a/examples/demo_pods/src/utils/maskValue.ts b/examples/demo_pods/src/utils/maskValue.ts deleted file mode 100644 index 12d1582..0000000 --- a/examples/demo_pods/src/utils/maskValue.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Mirrors the trim/strict-equals check used elsewhere so a stray newline -// or whitespace in `.env` doesn't accidentally enable masking. -const E2E_MODE = (import.meta.env.VITE_E2E_MODE ?? '').trim() === 'true'; -const MASK_CHAR = '•'; - -export function maskValue(value: string): string { - if (E2E_MODE) { - return MASK_CHAR.repeat(value.length); - } - return value; -} diff --git a/ios/Sources/OneSignalCapacitorPlugin/OneSignalCapacitorPlugin.swift b/ios/Sources/OneSignalCapacitorPlugin/OneSignalCapacitorPlugin.swift index d11e855..c4f21ea 100644 --- a/ios/Sources/OneSignalCapacitorPlugin/OneSignalCapacitorPlugin.swift +++ b/ios/Sources/OneSignalCapacitorPlugin/OneSignalCapacitorPlugin.swift @@ -110,7 +110,7 @@ public class OneSignalCapacitorPlugin: CAPPlugin, CAPBridgedPlugin { initialized = true OneSignalWrapper.sdkType = "capacitor" - OneSignalWrapper.sdkVersion = "010004" + OneSignalWrapper.sdkVersion = "010005" // OSCapacitorLaunchOptions's +load captures the dictionary from // UIApplicationDidFinishLaunchingNotification at process start (before // main()), so cold-start notification taps that arrive via launchOptions diff --git a/package.json b/package.json index cc663bf..992fb7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@onesignal/capacitor-plugin", - "version": "1.0.4", + "version": "1.0.5", "description": "OneSignal is a high volume Push Notification service for mobile apps. This is the pure Capacitor plugin for OneSignal, providing push notifications, in-app messaging, and more.", "keywords": [ "apns", @@ -60,7 +60,6 @@ "@capacitor/docgen": "^0.2.2", "@types/bun": "latest", "@vitest/coverage-v8": "^4.1.2", - "happy-dom": "^20.9.0", "typescript": "^5.9.3", "vite-plus": "0.1.20" }, diff --git a/vite.config.ts b/vite.config.ts index 5a580bf..593cb3f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -32,7 +32,7 @@ export default defineConfig({ }, test: { clearMocks: true, - environment: 'happy-dom', + environment: 'node', include: ['**/*.test.ts', '**/*.test.tsx'], coverage: { exclude: ['mocks/**', 'src/helpers.ts'],