@@ -5,6 +5,18 @@ import { UserData, userDataFromJson } from '../models/UserData';
55
66const DEFAULT_ANDROID_CHANNEL_ID = 'b3b015d9-c050-4042-8548-dcc34aa44aa4' ;
77
8+ function isTransientSendFailure ( data : unknown ) : boolean {
9+ if ( ! data || typeof data !== 'object' ) return false ;
10+ const record = data as { id ?: unknown ; errors ?: unknown ; recipients ?: unknown } ;
11+ const errors = record . errors ;
12+ const hasErrors =
13+ ( Array . isArray ( errors ) && errors . length > 0 ) ||
14+ ( errors != null && typeof errors === 'object' && Object . keys ( errors ) . length > 0 ) ;
15+ const missingId = typeof record . id !== 'string' || record . id . length === 0 ;
16+ const zeroRecipients = typeof record . recipients === 'number' && record . recipients === 0 ;
17+ return hasErrors || missingId || zeroRecipients ;
18+ }
19+
820class OneSignalApiService {
921 private static _instance : OneSignalApiService ;
1022 private _appId : string = '' ;
@@ -75,35 +87,59 @@ class OneSignalApiService {
7587 subscriptionId : string ,
7688 extra : Record < string , unknown > ,
7789 ) : Promise < boolean > {
78- try {
79- const body = {
80- app_id : this . _appId ,
81- include_subscription_ids : [ subscriptionId ] ,
82- headings,
83- contents,
84- ...extra ,
85- } ;
86-
87- const response = await fetch ( 'https://onesignal.com/api/v1/notifications' , {
88- method : 'POST' ,
89- headers : {
90- Accept : 'application/vnd.onesignal.v1+json' ,
91- 'Content-Type' : 'application/json' ,
92- } ,
93- body : JSON . stringify ( body ) ,
94- } ) ;
95-
96- if ( ! response . ok ) {
97- const text = await response . text ( ) ;
98- console . error ( `Send notification failed: ${ text } ` ) ;
90+ const body = {
91+ app_id : this . _appId ,
92+ include_subscription_ids : [ subscriptionId ] ,
93+ headings,
94+ contents,
95+ ...extra ,
96+ } ;
97+
98+ const maxAttempts = 3 ;
99+
100+ // Retry while the OneSignal backend hasn't yet indexed the freshly
101+ // created subscription. The /notifications endpoint reports this race in
102+ // a few different shapes, all of which return HTTP 200:
103+ // {"id":"...","recipients":0} (user just switched, push token not yet attached)
104+ // {"id":"...","errors":{"invalid_player_ids":[...]}}
105+ // {"id":"","errors":["All included players are not subscribed"]}
106+ // {"id":"","errors":[...]}
107+ // Treat any 200 response with no real id, populated errors, or recipients=0 as transient.
108+ for ( let attempt = 1 ; attempt <= maxAttempts ; attempt ++ ) {
109+ try {
110+ const response = await fetch ( 'https://onesignal.com/api/v1/notifications' , {
111+ method : 'POST' ,
112+ headers : {
113+ Accept : 'application/vnd.onesignal.v1+json' ,
114+ 'Content-Type' : 'application/json' ,
115+ } ,
116+ body : JSON . stringify ( body ) ,
117+ } ) ;
118+
119+ if ( ! response . ok ) {
120+ const text = await response . text ( ) ;
121+ console . error ( `Send notification failed: ${ text } ` ) ;
122+ return false ;
123+ }
124+
125+ const data = await response . json ( ) . catch ( ( ) => undefined ) ;
126+ if ( isTransientSendFailure ( data ) ) {
127+ if ( attempt < maxAttempts ) {
128+ await new Promise < void > ( ( resolve ) => setTimeout ( ( ) => resolve ( ) , 3_000 * attempt ) ) ;
129+ continue ;
130+ }
131+ console . error ( `Send notification failed: ${ JSON . stringify ( data ) } ` ) ;
132+ return false ;
133+ }
134+
135+ return true ;
136+ } catch ( err ) {
137+ console . error ( `Send notification error: ${ String ( err ) } ` ) ;
99138 return false ;
100139 }
101-
102- return true ;
103- } catch ( err ) {
104- console . error ( `Send notification error: ${ String ( err ) } ` ) ;
105- return false ;
106140 }
141+
142+ return false ;
107143 }
108144
109145 async updateLiveActivity (
0 commit comments