ReviewOS

also looking at this

craft-native/craft

fix(ios): hand a notification tap that launches the app to the page that asks for it

#255
Merged glennmichael123 wants to merge fix/ios-notification-tap-cold-start into main
5 files +194 -9
packages/ios/src/page-script.test.tsmodified+52-0
Changes to packages/ios/src/page-script.test.ts
@@ -91,6 +91,9 @@ function loadPage(beforeInject?: (page: Record<string, any>) => void, seed = SEE
9191 // What DeepLinkManager.dispatchDeepLink evaluates, once the script has run.
9292 link: (url: string, initial: boolean) =>
9393 page.dispatchEvent(new CustomEvent('craftDeepLink', { detail: { url, initial } })),
94 // What CraftEventManager.handleNotificationResponse evaluates.
95 tap: (detail: unknown) =>
96 page.dispatchEvent(new CustomEvent('craftNotificationResponse', { detail })),
9497 }
9598}
9699
@@ -201,6 +204,55 @@ describe('the injected iOS page script', () => {
201204 })
202205 }
203206
207 // A notification tap on a cold launch is flushed the moment the bridge is
208 // ready, and a page that wires its listener after its router hydrates was
209 // not listening yet: "Try it" opened the home screen instead. Held the way
210 // deep links are (#198), and handed to the first subscriber.
211 it('hands a tap that arrived before anyone subscribed to the first subscriber', async () => {
212 const page = loadPage()
213 page.tap({ screen: 'plant-id' })
214
215 const seen: unknown[] = []
216 page.craft.notifications.onTap((detail: unknown) => seen.push(detail))
217 await tick()
218
219 expect(seen).toEqual([{ screen: 'plant-id' }])
220 })
221
222 it('keeps onTap on the notifications object the page ends up with', () => {
223 // installCraftMobileContract replaces craft.notifications after the
224 // replay attaches onTap, and onTap survives only because it copies own
225 // properties across. Reorder those two blocks and it would vanish with
226 // nothing else failing, so this reads the final object.
227 const page = loadPage()
228 expect(typeof page.craft.notifications.onTap).toBe('function')
229 expect(typeof page.craft.notifications.onTap(() => {})).toBe('function')
230 // And the contract's own additions are still there beside it.
231 expect(typeof page.craft.notifications.show).toBe('function')
232 })
233
234 it('delivers a tap once, live, to a page already subscribed', async () => {
235 const page = loadPage()
236 const seen: unknown[] = []
237 page.craft.notifications.onTap((detail: unknown) => seen.push(detail))
238 await tick()
239
240 page.tap({ screen: 'recap' })
241 expect(seen).toEqual([{ screen: 'recap' }])
242 })
243
244 it('drops held taps for a subscriber that unsubscribed in the same tick', async () => {
245 const page = loadPage()
246 page.tap({ screen: 'plant-id' })
247
248 const seen: unknown[] = []
249 const unsubscribe = page.craft.notifications.onTap((detail: unknown) => seen.push(detail))
250 unsubscribe()
251 await tick()
252
253 expect(seen).toEqual([])
254 })
255
204256 // #198 and #215. The Android page script runs the same block; its own test
205257 // covers the rest of the contract.
206258 const tick = () => new Promise(resolve => setTimeout(resolve, 5))