diff --git a/.github/actions/setup-demo/action.yml b/.github/actions/setup-demo/action.yml index 708be879..9b52b677 100644 --- a/.github/actions/setup-demo/action.yml +++ b/.github/actions/setup-demo/action.yml @@ -54,5 +54,4 @@ runs: run: | echo "ONESIGNAL_APP_ID=${{ inputs.onesignal-app-id }}" > .env echo "ONESIGNAL_API_KEY=${{ inputs.onesignal-api-key }}" >> .env - echo "E2E_MODE=true" >> .env echo "ONESIGNAL_ANDROID_CHANNEL_ID=7ec2ece9-c538-4656-9516-1316f48a005c" >> .env diff --git a/android/build.gradle b/android/build.gradle index e8e218c0..7e32d134 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -39,7 +39,7 @@ dependencies { // Exclude OkHttp from OneSignal's transitive deps: the otel module pulls in OkHttp 5.x // (via opentelemetry-exporter-sender-okhttp) which is binary-incompatible with React Native's // networking stack (okhttp3.internal.Util removed in 5.x). React Native already provides OkHttp 4.x. - api('com.onesignal:OneSignal:5.8.1') { + api('com.onesignal:OneSignal:5.9.2') { exclude group: 'com.squareup.okhttp3', module: 'okhttp' } diff --git a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java index 27e0904d..448af37d 100644 --- a/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java +++ b/android/src/main/java/com/onesignal/rnonesignalandroid/RNOneSignal.java @@ -230,7 +230,7 @@ public void invalidate() { @Override public void initialize(String appId) { OneSignalWrapper.setSdkType("reactnative"); - OneSignalWrapper.setSdkVersion("050405"); + OneSignalWrapper.setSdkVersion("050406"); if (oneSignalInitDone) { Logging.debug("Already initialized the OneSignal React-Native SDK", null); diff --git a/examples/demo/.env.example b/examples/demo/.env.example index 389f07e0..10c390f0 100644 --- a/examples/demo/.env.example +++ b/examples/demo/.env.example @@ -1,7 +1,6 @@ # Default App ID (used if ONESIGNAL_APP_ID is empty): 77e32082-ea27-42e3-a898-c72e141824ef ONESIGNAL_APP_ID=your-onesignal-app-id ONESIGNAL_API_KEY=your-onesignal-api-key -E2E_MODE=false # Optional: Android Notification Channel ID for the WITH SOUND test notification. # Create one in your OneSignal dashboard under Settings > Android Notification Categories. diff --git a/examples/demo/src/components/SectionCard.tsx b/examples/demo/src/components/SectionCard.tsx index 18f60ca9..6ccd5ba0 100644 --- a/examples/demo/src/components/SectionCard.tsx +++ b/examples/demo/src/components/SectionCard.tsx @@ -19,7 +19,7 @@ export default function SectionCard({ title, children, onInfoTap, sectionKey, st {onInfoTap && ( @@ -50,6 +50,13 @@ const styles = StyleSheet.create({ letterSpacing: 0.5, textTransform: 'uppercase', }, + infoButton: { + width: 32, + height: 32, + alignItems: 'center', + justifyContent: 'center', + marginRight: -11, + }, infoIcon: { fontSize: 18, color: AppColors.osGrey500, diff --git a/examples/demo/src/components/sections/AppSection.tsx b/examples/demo/src/components/sections/AppSection.tsx index 525b2ee9..03915d37 100644 --- a/examples/demo/src/components/sections/AppSection.tsx +++ b/examples/demo/src/components/sections/AppSection.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { View, Text, TouchableOpacity, Linking, StyleSheet } from 'react-native'; import { AppColors, AppTextStyles, AppTheme, AppSpacing } from '../../theme'; -import { maskValue } from '../../utils/maskValue'; import SectionCard from '../SectionCard'; import ToggleRow from '../ToggleRow'; @@ -33,7 +32,7 @@ export default function AppSection({ ellipsizeMode="middle" testID="app_id_value" > - {maskValue(appId)} + {appId} diff --git a/examples/demo/src/components/sections/PushSection.tsx b/examples/demo/src/components/sections/PushSection.tsx index ea3fec02..56e52c78 100644 --- a/examples/demo/src/components/sections/PushSection.tsx +++ b/examples/demo/src/components/sections/PushSection.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { AppColors, AppTextStyles, AppTheme, AppSpacing } from '../../theme'; -import { maskValue } from '../../utils/maskValue'; import ActionButton from '../ActionButton'; import SectionCard from '../SectionCard'; import ToggleRow from '../ToggleRow'; @@ -35,7 +34,7 @@ export default function PushSection({ ellipsizeMode="middle" testID="push_id_value" > - {maskValue(pushSubscriptionId ?? '—')} + {pushSubscriptionId ?? '—'} diff --git a/examples/demo/src/hooks/useOneSignal.ts b/examples/demo/src/hooks/useOneSignal.ts index f8c774b9..bd1359c0 100644 --- a/examples/demo/src/hooks/useOneSignal.ts +++ b/examples/demo/src/hooks/useOneSignal.ts @@ -62,6 +62,7 @@ export type UseOneSignalReturn = { consentRequired: boolean; privacyConsentGiven: boolean; externalUserId: string | undefined; + oneSignalId: string | undefined; pushSubscriptionId: string | undefined; isPushEnabled: boolean; hasNotificationPermission: boolean; @@ -115,6 +116,7 @@ function useOneSignalState(): UseOneSignalReturn { const [consentRequired, setConsentRequiredState] = useState(false); const [privacyConsentGiven, setPrivacyConsentGivenState] = useState(false); 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); @@ -207,6 +209,8 @@ function useOneSignalState(): UseOneSignalReturn { `User changed: onesignalId=${nextOnesignalId ?? 'null'}, externalId=${event.current.externalId ?? 'null'}`, ); + setOneSignalId(nextOnesignalId ?? undefined); + if (nextOnesignalId === null) { return; } @@ -280,6 +284,7 @@ function useOneSignalState(): UseOneSignalReturn { setIsReady(true); const initialOnesignalId = await OneSignal.User.getOnesignalId(); + setOneSignalId(initialOnesignalId ?? undefined); if (initialOnesignalId) { await fetchUserDataFromApi(); } @@ -535,6 +540,7 @@ function useOneSignalState(): UseOneSignalReturn { consentRequired, privacyConsentGiven, externalUserId, + oneSignalId, pushSubscriptionId, isPushEnabled, hasNotificationPermission, diff --git a/examples/demo/src/screens/HomeScreen.tsx b/examples/demo/src/screens/HomeScreen.tsx index 38538ceb..1e052225 100644 --- a/examples/demo/src/screens/HomeScreen.tsx +++ b/examples/demo/src/screens/HomeScreen.tsx @@ -28,13 +28,14 @@ import { AppColors } from '../theme'; export default function HomeScreen() { const navigation = useNavigation(); const os = useOneSignal(); + const { isReady, promptPush } = os; const [tooltipVisible, setTooltipVisible] = useState(false); const [activeTooltip, setActiveTooltip] = useState(null); useEffect(() => { - if (os.isReady) os.promptPush(); - }, [os.isReady, os.promptPush]); + if (isReady) promptPush(); + }, [isReady, promptPush]); const showTooltipModal = (key: string) => { const tooltip = TooltipHelper.getInstance().getTooltip(key); diff --git a/examples/demo/src/services/OneSignalApiService.ts b/examples/demo/src/services/OneSignalApiService.ts index ce03cc12..fc5a592d 100644 --- a/examples/demo/src/services/OneSignalApiService.ts +++ b/examples/demo/src/services/OneSignalApiService.ts @@ -5,6 +5,17 @@ import { UserData, userDataFromJson } from '../models/UserData'; const DEFAULT_ANDROID_CHANNEL_ID = 'b3b015d9-c050-4042-8548-dcc34aa44aa4'; +function isTransientSendFailure(data: unknown): boolean { + if (!data || typeof data !== 'object') return false; + const record = data as { id?: unknown; errors?: unknown }; + const errors = record.errors; + const hasErrors = + (Array.isArray(errors) && errors.length > 0) || + (errors != null && typeof errors === 'object' && Object.keys(errors).length > 0); + const missingId = typeof record.id !== 'string' || record.id.length === 0; + return hasErrors || missingId; +} + class OneSignalApiService { private static _instance: OneSignalApiService; private _appId: string = ''; @@ -75,35 +86,58 @@ class OneSignalApiService { subscriptionId: string, extra: Record, ): Promise { - try { - const body = { - app_id: this._appId, - include_subscription_ids: [subscriptionId], - headings, - contents, - ...extra, - }; - - const response = await fetch('https://onesignal.com/api/v1/notifications', { - method: 'POST', - headers: { - Accept: 'application/vnd.onesignal.v1+json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - const text = await response.text(); - console.error(`Send notification failed: ${text}`); + const body = { + app_id: this._appId, + include_subscription_ids: [subscriptionId], + headings, + contents, + ...extra, + }; + + const maxAttempts = 3; + + // Retry while the OneSignal backend hasn't yet indexed the freshly + // created subscription. The /notifications endpoint reports this race in + // a few different shapes, all of which return HTTP 200: + // {"errors":{"invalid_player_ids":[...]}} + // {"id":"","errors":["All included players are not subscribed"]} + // {"id":"","errors":[...]} + // Treat any 200 response without a real notification id as transient. + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const response = await fetch('https://onesignal.com/api/v1/notifications', { + method: 'POST', + headers: { + Accept: 'application/vnd.onesignal.v1+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const text = await response.text(); + console.error(`Send notification failed: ${text}`); + return false; + } + + const data = await response.json().catch(() => undefined); + if (isTransientSendFailure(data)) { + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(() => resolve(), 3_000 * attempt)); + continue; + } + console.error(`Send notification failed: ${JSON.stringify(data)}`); + 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 bab93102..00000000 --- a/examples/demo/src/utils/maskValue.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { E2E_MODE } from '@env'; - -const MASK_CHAR = '•'; - -export function maskValue(value: string): string { - if (E2E_MODE === 'true' && value !== '—') { - return MASK_CHAR.repeat(value.length); - } - return value; -} diff --git a/examples/demo/types/env.d.ts b/examples/demo/types/env.d.ts index 6ee94c30..2cfe0569 100644 --- a/examples/demo/types/env.d.ts +++ b/examples/demo/types/env.d.ts @@ -2,5 +2,4 @@ declare module '@env' { export const ONESIGNAL_APP_ID: string; export const ONESIGNAL_API_KEY: string; export const ONESIGNAL_ANDROID_CHANNEL_ID: string; - export const E2E_MODE: string; } diff --git a/ios/RCTOneSignal/RCTOneSignal.mm b/ios/RCTOneSignal/RCTOneSignal.mm index ccc176ec..6a2db226 100644 --- a/ios/RCTOneSignal/RCTOneSignal.mm +++ b/ios/RCTOneSignal/RCTOneSignal.mm @@ -23,7 +23,7 @@ - (void)initOneSignal:(NSDictionary *)launchOptions { return; OneSignalWrapper.sdkType = @"reactnative"; - OneSignalWrapper.sdkVersion = @"050405"; + OneSignalWrapper.sdkVersion = @"050406"; // initialize the SDK with a nil app ID so cold start click listeners can be // triggered [OneSignal initialize:nil withLaunchOptions:launchOptions]; diff --git a/package.json b/package.json index 50769926..061118fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-onesignal", - "version": "5.4.5", + "version": "5.4.6", "description": "React Native OneSignal SDK", "keywords": [ "android",