@marktiderman/genesis-native 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -1
- package/dist/{block-C5-noMhB.d.ts → block-Onqx7NiK.d.ts} +1 -1
- package/dist/{blocks-a1YQ1Elf.d.ts → blocks-C2RDpvaN.d.ts} +59 -4
- package/dist/chunk-ONAXQHNG.js +23 -0
- package/dist/chunk-ONAXQHNG.js.map +1 -0
- package/dist/devtools.d.ts +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/observability.d.ts +3 -3
- package/dist/push-payload.d.ts +43 -0
- package/dist/push-payload.js +3 -0
- package/dist/push-payload.js.map +1 -0
- package/dist/push.d.ts +21 -3
- package/dist/push.js +77 -4
- package/dist/push.js.map +1 -1
- package/dist/rich-push-plugin.d.ts +74 -0
- package/dist/rich-push-plugin.js +209 -0
- package/dist/rich-push-plugin.js.map +1 -0
- package/dist/updates.d.ts +1 -1
- package/ios/notification-service/NotificationService.swift +309 -0
- package/package.json +23 -7
- package/src/plugin/rich-push.ts +312 -0
- package/src/push/backend.ts +10 -2
- package/src/push/block.tsx +61 -2
- package/src/push/index.ts +4 -1
- package/src/push/modules.ts +13 -1
- package/src/push/payload.ts +61 -0
- package/src/push/registration.ts +37 -1
- package/src/push/routing.ts +30 -0
package/README.md
CHANGED
|
@@ -430,7 +430,29 @@ permission as its subtitle; `settingsRow` changes or drops it) and a boot step
|
|
|
430
430
|
that reads the permission behind the splash (a second at most; it never fails
|
|
431
431
|
the boot), and names its migration. It runs on migration
|
|
432
432
|
0005 and the `genesis-push-send` function of
|
|
433
|
-
[`@marktiderman/genesis-supabase`](../supabase/README.md#push-the-push-block)
|
|
433
|
+
[`@marktiderman/genesis-supabase`](../supabase/README.md#push-the-push-block),
|
|
434
|
+
or on [`@marktiderman/genesis-notifications`](../notifications/README.md), which
|
|
435
|
+
now owns the server side of push (it adopts 0005's tables and sends through
|
|
436
|
+
one runner). With the notifier, pass its backend and the registry's categories:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
import * as Crypto from "expo-crypto";
|
|
440
|
+
import { notificationsPushBackend, pushOptionsFrom } from "@marktiderman/genesis-notifications/native";
|
|
441
|
+
|
|
442
|
+
createPush({
|
|
443
|
+
…,
|
|
444
|
+
backend: notificationsPushBackend(supabase), // registers with this install's id
|
|
445
|
+
...pushOptionsFrom(notifications), // categories (Android channels) and device categories (action buttons)
|
|
446
|
+
randomUUID: Crypto.randomUUID, // the install id; Hermes has no crypto.randomUUID
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`deviceCategories` registers notification categories with their buttons (Reply
|
|
451
|
+
with a text field, Mark as read) when the block starts; a button tap reaches
|
|
452
|
+
`onResponse`, and `notificationAction(response)` reads it: the action id, the
|
|
453
|
+
typed text, the link and the notifier's notification id. A backend that sets
|
|
454
|
+
`usesInstallationId` gets this install's id (made once, kept across sign-ins)
|
|
455
|
+
as `register`'s fourth argument; `supabasePushBackend` doesn't.
|
|
434
456
|
|
|
435
457
|
```ts
|
|
436
458
|
// lib/push.ts
|
|
@@ -541,6 +563,100 @@ Not covered: delivery through Apple's and Google's push services hasn't been
|
|
|
541
563
|
tried (it needs an EAS project with APNs and FCM credentials), and there is no
|
|
542
564
|
in-app inbox: that is `inbox-thread` in the repo's `features.ts`.
|
|
543
565
|
|
|
566
|
+
### iOS rich push: photos, the sender's avatar, Communication Notifications
|
|
567
|
+
|
|
568
|
+
`@marktiderman/genesis-native/rich-push-plugin` is an Expo config plugin that
|
|
569
|
+
adds a Notification Service Extension to the iOS app. iOS runs the extension
|
|
570
|
+
on each push that says `mutable-content: 1`, before it is shown, for up to
|
|
571
|
+
about 30 seconds. The extension:
|
|
572
|
+
|
|
573
|
+
- **attaches pictures.** It downloads `data.image` and `data.senderAvatar`
|
|
574
|
+
when they are https URLs (no credentials, at most 1024 characters; JPEG, PNG
|
|
575
|
+
or GIF; at most 10 MB) and attaches them. Only the avatar is read into memory
|
|
576
|
+
(at most 1 MB; a bigger one is still attached, but not drawn as the sender),
|
|
577
|
+
because the extension gets about 24 MB. A download that fails or is
|
|
578
|
+
late is skipped. The notification is always shown, with what the payload
|
|
579
|
+
already said, and is never dropped.
|
|
580
|
+
- **makes chat pushes Communication Notifications.** A push in the `messages`
|
|
581
|
+
category with a `senderName` becomes an incoming `INSendMessageIntent`. It
|
|
582
|
+
carries the sender (`senderName`, `senderAvatar`, and `senderId` as the
|
|
583
|
+
handle), the conversation (`threadId`, else `data.conversationId`) and, in a
|
|
584
|
+
group, `groupName` (iOS draws its generic group picture). The extension then
|
|
585
|
+
calls `content.updating(from:)`. The
|
|
586
|
+
sender's photo shows on the notification, and Focus treats it as a message
|
|
587
|
+
(a person can let a Focus through). On a `messages` push the avatar is the
|
|
588
|
+
sender's photo; on any other push it is attached.
|
|
589
|
+
- **downloads only what you allow.** With `imageHosts`, images come only from
|
|
590
|
+
those hosts and their subdomains. Without it, any https host is used. Set it
|
|
591
|
+
when a URL in a push can be one a person chose (an avatar URL they typed),
|
|
592
|
+
because that host would otherwise see each delivery.
|
|
593
|
+
- **never fetches content.** It reads only the payload. With hidden previews
|
|
594
|
+
(`hidePreviews`, applied by the sender), the server sends neutral text and no
|
|
595
|
+
sender, group or picture fields, so there is nothing to show. If the
|
|
596
|
+
communication update is refused (for example, no entitlement), the plain
|
|
597
|
+
notification shows instead.
|
|
598
|
+
|
|
599
|
+
**Setup.** Add the plugin after any plugin that changes the bundle id (the
|
|
600
|
+
Genesis plugin's `APP_VARIANT` suffix), then prebuild:
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
plugins: [
|
|
604
|
+
"expo-notifications",
|
|
605
|
+
"@marktiderman/genesis-cli",
|
|
606
|
+
["@marktiderman/genesis-native/rich-push-plugin", { imageHosts: ["yourproject.supabase.co"] }], // also: targetName, bundleIdSuffix, communication, deploymentTarget
|
|
607
|
+
],
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
It runs on the app's own `expo` (`expo/config-plugins`), so it needs nothing
|
|
611
|
+
else installed.
|
|
612
|
+
|
|
613
|
+
What it sets:
|
|
614
|
+
|
|
615
|
+
- **App:** the `com.apple.developer.usernotifications.communication`
|
|
616
|
+
entitlement and `NSUserActivityTypes: ["INSendMessageIntent"]` in
|
|
617
|
+
Info.plist. Both are skipped with `communication: false`, which keeps
|
|
618
|
+
attachments only.
|
|
619
|
+
- **Extension:** a target named `GenesisNotificationService` in
|
|
620
|
+
`ios/GenesisNotificationService/` (the Swift from this package's
|
|
621
|
+
`ios/notification-service/`, an Info.plist declaring `INSendMessageIntent`
|
|
622
|
+
and `GenesisRichPushImageHosts`, and entitlements). Its bundle id
|
|
623
|
+
is `<app id>.NotificationService`. It uses the app's version, build number,
|
|
624
|
+
minimum iOS and team. The app embeds it and depends on it.
|
|
625
|
+
- **EAS:** `extra.eas.build.experimental.ios.appExtensions`. The extension is
|
|
626
|
+
a separate bundle id, so it needs its own provisioning profile. EAS Build
|
|
627
|
+
reads this entry and creates the App ID, its capability and the profile with
|
|
628
|
+
the app's. You don't create them by hand. A local `expo run:ios` on a device
|
|
629
|
+
needs an Apple team (`ios.appleTeamId`) that can sign both.
|
|
630
|
+
|
|
631
|
+
**The payload contract.** The sender sets `mutableContent: true`, or the
|
|
632
|
+
extension never runs. `richPushMessage(message, { image, senderAvatar })`
|
|
633
|
+
(also exported from `/push-payload`, which imports no React, for servers and
|
|
634
|
+
edge functions) sets it and drops image URLs that aren't https. For chat,
|
|
635
|
+
`chatPushPayload` from `@marktiderman/genesis-chat/push` builds the whole set:
|
|
636
|
+
|
|
637
|
+
| Field | Where | Read by |
|
|
638
|
+
| --- | --- | --- |
|
|
639
|
+
| `mutableContent: true` | message (`mutable-content: 1`) | iOS: runs the extension |
|
|
640
|
+
| `categoryId: "messages"` | message (`aps.category`) | the extension (Communication Notification), Reply and Mark read |
|
|
641
|
+
| `threadId` | message (`aps.thread-id`), the conversation id | grouping, and the intent's conversation |
|
|
642
|
+
| `data.image` | https | attached |
|
|
643
|
+
| `data.senderAvatar` | https | the sender's photo (`messages`), else attached |
|
|
644
|
+
| `data.senderName`, `data.senderId` | text, an opaque id | the intent's sender; without a name the push stays plain |
|
|
645
|
+
| `data.groupName` | text, groups only | the intent's group name |
|
|
646
|
+
| `data.conversationId`, `data.url`, `data.seq`, `data.messageId` | ids and the link | tap routing, Reply, Mark read |
|
|
647
|
+
|
|
648
|
+
APNs takes 4 KB per push, so keep the URLs short (a storage path, not a long
|
|
649
|
+
signed URL). Expo's push service delivers `data` under the APNs payload's
|
|
650
|
+
`body` key. The
|
|
651
|
+
extension reads it there, or from the payload's top level for a push sent to
|
|
652
|
+
APNs directly.
|
|
653
|
+
|
|
654
|
+
Not covered: the extension has been built and run on the iOS Simulator only.
|
|
655
|
+
It hasn't been run on a device or from an EAS build. Whether EAS's build number
|
|
656
|
+
increment reaches the extension (its `CURRENT_PROJECT_VERSION` is set at
|
|
657
|
+
prebuild) hasn't been checked either. Donated conversations stay in Siri's
|
|
658
|
+
suggestions after a person turns previews off.
|
|
659
|
+
|
|
544
660
|
## Blocks
|
|
545
661
|
|
|
546
662
|
`blocks` takes `defineBlock` declarations (`@marktiderman/genesis-features/blocks`) a native shell
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ComponentType, ReactNode } from 'react';
|
|
3
|
-
import { N as NativeBlock, n as ErrorReport, x as NativeBlockRootProps } from './blocks-
|
|
3
|
+
import { N as NativeBlock, n as ErrorReport, x as NativeBlockRootProps } from './blocks-C2RDpvaN.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Typed analytics events: the app lists every event and the props each may
|
|
@@ -878,8 +878,16 @@ interface QuietHours {
|
|
|
878
878
|
}
|
|
879
879
|
declare const DEFAULT_PREFERENCES: NotificationPreferences;
|
|
880
880
|
interface PushBackend {
|
|
881
|
-
/**
|
|
882
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Keep this device's token against the signed-in person. `installationId`
|
|
883
|
+
* is this install's random id (kept on the phone across sign-ins): a
|
|
884
|
+
* backend that checks it moves a token another login holds only when the
|
|
885
|
+
* id matches (the same phone). `supabasePushBackend` (migration 0005)
|
|
886
|
+
* doesn't send it; genesis-notifications' backend does.
|
|
887
|
+
*/
|
|
888
|
+
register(token: string, platform: string, appVersion: string | null, installationId?: string): Promise<void>;
|
|
889
|
+
/** Send `installationId` to `register` (the block makes and keeps one). Default false. */
|
|
890
|
+
readonly usesInstallationId?: boolean;
|
|
883
891
|
/** Forget this device's token; works signed out (holding the token is the proof). */
|
|
884
892
|
forget(token: string): Promise<void>;
|
|
885
893
|
/** The signed-in person's preferences, or `null` before they have saved any. */
|
|
@@ -951,6 +959,22 @@ interface NotificationLike {
|
|
|
951
959
|
interface NotificationResponseLike {
|
|
952
960
|
notification: NotificationLike;
|
|
953
961
|
actionIdentifier: string;
|
|
962
|
+
/** What the person typed, for an action with a text input (Reply). */
|
|
963
|
+
userText?: string;
|
|
964
|
+
}
|
|
965
|
+
/** `NotificationAction` (expo-notifications 58): one button of a notification category. */
|
|
966
|
+
interface NotificationActionLike {
|
|
967
|
+
identifier: string;
|
|
968
|
+
buttonTitle: string;
|
|
969
|
+
textInput?: {
|
|
970
|
+
placeholder: string;
|
|
971
|
+
submitButtonTitle: string;
|
|
972
|
+
};
|
|
973
|
+
options?: {
|
|
974
|
+
isAuthenticationRequired?: boolean;
|
|
975
|
+
isDestructive?: boolean;
|
|
976
|
+
opensAppToForeground?: boolean;
|
|
977
|
+
};
|
|
954
978
|
}
|
|
955
979
|
/** `NotificationBehavior` without the deprecated `shouldShowAlert`. */
|
|
956
980
|
interface NotificationBehaviorLike {
|
|
@@ -990,6 +1014,10 @@ interface NotificationsModuleLike {
|
|
|
990
1014
|
getLastNotificationResponse(): NotificationResponseLike | null;
|
|
991
1015
|
clearLastNotificationResponse(): void;
|
|
992
1016
|
setBadgeCountAsync(count: number): Promise<boolean>;
|
|
1017
|
+
/** Registers a notification category (its action buttons). Optional: older fakes leave it out. */
|
|
1018
|
+
setNotificationCategoryAsync?(identifier: string, actions: NotificationActionLike[], options?: {
|
|
1019
|
+
previewPlaceholder?: string;
|
|
1020
|
+
}): Promise<unknown>;
|
|
993
1021
|
getBadgeCountAsync(): Promise<number>;
|
|
994
1022
|
/** `"expo.modules.notifications.actions.DEFAULT"`: the person tapped the notification itself. */
|
|
995
1023
|
DEFAULT_ACTION_IDENTIFIER: string;
|
|
@@ -1007,7 +1035,7 @@ interface PushAppStateLike {
|
|
|
1007
1035
|
};
|
|
1008
1036
|
}
|
|
1009
1037
|
/** Where a reported error came from. */
|
|
1010
|
-
type PushErrorSource = "permission" | "register" | "forget" | "preferences" | "badge" | "channels" | "routing" | "storage" | "setup";
|
|
1038
|
+
type PushErrorSource = "permission" | "register" | "forget" | "preferences" | "badge" | "channels" | "categories" | "routing" | "storage" | "setup";
|
|
1011
1039
|
type PushOnError = (error: Error, source: PushErrorSource) => void;
|
|
1012
1040
|
|
|
1013
1041
|
/**
|
|
@@ -1176,6 +1204,8 @@ declare function createRegistration(options: {
|
|
|
1176
1204
|
/** Is this user still the one signed in? A sync for someone who has signed out is dropped. */
|
|
1177
1205
|
isCurrent: (userId: string) => boolean;
|
|
1178
1206
|
now: () => number;
|
|
1207
|
+
/** Makes this install's id (once; it is kept). Default `crypto.randomUUID`. */
|
|
1208
|
+
randomUUID?: () => string;
|
|
1179
1209
|
onError?: PushOnError;
|
|
1180
1210
|
}): Registration;
|
|
1181
1211
|
|
|
@@ -1200,6 +1230,22 @@ interface PushChannel {
|
|
|
1200
1230
|
description?: string;
|
|
1201
1231
|
importance?: ChannelImportance;
|
|
1202
1232
|
}
|
|
1233
|
+
/** A notification category and its buttons. */
|
|
1234
|
+
interface PushDeviceCategory {
|
|
1235
|
+
identifier: string;
|
|
1236
|
+
actions: readonly {
|
|
1237
|
+
id: string;
|
|
1238
|
+
title: string;
|
|
1239
|
+
textInput?: {
|
|
1240
|
+
placeholder?: string;
|
|
1241
|
+
submitTitle?: string;
|
|
1242
|
+
};
|
|
1243
|
+
opensApp?: boolean;
|
|
1244
|
+
destructive?: boolean;
|
|
1245
|
+
requiresAuth?: boolean;
|
|
1246
|
+
}[];
|
|
1247
|
+
previewPlaceholder?: string;
|
|
1248
|
+
}
|
|
1203
1249
|
type PushSlots = {
|
|
1204
1250
|
/** The soft pre-prompt, shown after `usePushPrompt().offer()`. Default `PushPromptSheet`. */
|
|
1205
1251
|
pushPrompt: PushPromptProps;
|
|
@@ -1219,6 +1265,15 @@ interface PushOptions {
|
|
|
1219
1265
|
categories?: readonly NotificationCategory[];
|
|
1220
1266
|
/** More Android channels. A `default` channel is always made. */
|
|
1221
1267
|
channels?: readonly PushChannel[];
|
|
1268
|
+
/**
|
|
1269
|
+
* Notification categories with action buttons (Reply, Mark as read),
|
|
1270
|
+
* registered on the device when the block starts. A push names one in its
|
|
1271
|
+
* `categoryId`; a tap on a button reaches `onResponse` (`notificationAction`
|
|
1272
|
+
* reads it). genesis-notifications' registry makes these (`deviceCategories()`).
|
|
1273
|
+
*/
|
|
1274
|
+
deviceCategories?: readonly PushDeviceCategory[];
|
|
1275
|
+
/** Makes this install's id, for a backend that uses one (`usesInstallationId`). Default `crypto.randomUUID`; pass expo-crypto's on Hermes. */
|
|
1276
|
+
randomUUID?: () => string;
|
|
1222
1277
|
/** AsyncStorage: this device's token (to forget it after sign-out), the prompt's "Not now"s. */
|
|
1223
1278
|
storage?: PushStorageLike;
|
|
1224
1279
|
/** `AppState`: read the permission again on coming to the front (the person may have changed it in Settings). */
|
|
@@ -1537,4 +1592,4 @@ declare function useBlockSettings<R extends SettingsRowLike>(sections: readonly
|
|
|
1537
1592
|
/** The merge `useBlockSettings` does, without hooks (subtitles already read). @stability Beta */
|
|
1538
1593
|
declare function mergeBlockSettings<R extends SettingsRowLike>(sections: readonly BlockSettingsSection<R>[], rows: readonly Contributed<BlockSettingsRow>[], subtitles?: readonly (string | null | undefined)[], options?: UseBlockSettingsOptions): BlockSettingsSection<R | SettingsRowLike>[];
|
|
1539
1594
|
|
|
1540
|
-
export { defaultRejectionTracker as $, type AuthClientLike as A, type BlockSettingsSection as B, type SessionStatus as C, type DevtoolsFeedbackProps as D, type EnableRejectionTracker as E, FatalScreen as F, GenesisErrorBoundary as G, type HeldLinkAuth as H, type SessionStore as I, type SessionStoreOptions as J, type SettingsRowLike as K, type LinkHold as L, type SignedOutProps as M, type NativeBlock as N, type OnError as O, PlainFatalScreen as P, type SplashModule as Q, type UseBlockSettingsOptions as R, SessionProvider as S, type UseBootReturn as T, type UpdatesBlock as U, type VersionSignature as V, type UseHeldLinkOptions as W, createBoot as X, createLinkHold as Y, createSessionStore as Z, defaultErrorUtils as _, type DevtoolsSlots as a,
|
|
1595
|
+
export { defaultRejectionTracker as $, type AuthClientLike as A, type BlockSettingsSection as B, type SessionStatus as C, type DevtoolsFeedbackProps as D, type EnableRejectionTracker as E, FatalScreen as F, GenesisErrorBoundary as G, type HeldLinkAuth as H, type SessionStore as I, type SessionStoreOptions as J, type SettingsRowLike as K, type LinkHold as L, type SignedOutProps as M, type NativeBlock as N, type OnError as O, PlainFatalScreen as P, type SplashModule as Q, type UseBlockSettingsOptions as R, SessionProvider as S, type UseBootReturn as T, type UpdatesBlock as U, type VersionSignature as V, type UseHeldLinkOptions as W, createBoot as X, createLinkHold as Y, createSessionStore as Z, defaultErrorUtils as _, type DevtoolsSlots as a, usePush as a$, installGlobalErrorHandler as a0, installRejectionTracker as a1, mergeBlockSettings as a2, normalizeLink as a3, useBlockSettings as a4, useBlocks as a5, useBoot as a6, useHeldLink as a7, useSession as a8, type PushPermissionStatus as a9, type PushDeviceCategory as aA, type PushErrorSource as aB, type PushOnError as aC, type PushOptions as aD, type PushPromptProps as aE, PushPromptSheet as aF, PushProvider as aG, PushRoot as aH, type PushRootProps as aI, type PushSlots as aJ, type PushStorageLike as aK, type PushSupabaseLike as aL, type Registration as aM, type RegistrationState as aN, type RegistrationStatus as aO, type SubscriptionLike as aP, canPush as aQ, createPermissionStore as aR, createPreferencesStore as aS, createPromptStore as aT, createPush as aU, createRegistration as aV, statusOf as aW, supabasePushBackend as aX, useBadge as aY, useNotificationPreferences as aZ, useOptionalPush as a_, type NotificationResponseLike as aa, type NotificationLike as ab, type QuietHours as ac, type ChannelImportance as ad, type ChannelInputLike as ae, DEFAULT_PREFERENCES as af, type NotificationActionLike as ag, type NotificationBehaviorLike as ah, type NotificationCategory as ai, type NotificationPreferences as aj, type NotificationsModuleLike as ak, type PermissionRequestLike as al, type PermissionState as am, type PermissionStatusLike as an, type PermissionStore as ao, type PreferencesState as ap, type PreferencesStatus as aq, type PreferencesStore as ar, type PromptOptions as as, type PromptState as at, type PromptStore as au, type PushAppStateLike as av, type PushBackend as aw, PushBackendError as ax, type PushBlock as ay, type PushChannel as az, AuthGate as b, useCanReload as b$, usePushPermissionState as b0, usePushPrompt as b1, usePushRegistration as b2, useSuppressNotifications as b3, validPreferences as b4, type ApplicationLike as b5, CLOCK_SKEW_MS as b6, type ChannelSwitchResult as b7, type ChannelSwitcher as b8, type ChannelSwitcherOptions as b9, type UpdatesModuleLike as bA, type UpdatesOnError as bB, type UpdatesOptions as bC, UpdatesProvider as bD, UpdatesRoot as bE, type UpdatesRootProps as bF, type UpdatesSlots as bG, type UpdatesStorageLike as bH, type VersionSignatureInput as bI, WEEK_MS as bJ, type WhatsNewOptions as bK, type WhatsNewProps as bL, WhatsNewSheet as bM, type WhatsNewState as bN, type WhatsNewStore as bO, createChannelSwitcher as bP, createPolicyStore as bQ, createReloadGuard as bR, createUpdateChecker as bS, createUpdates as bT, createWhatsNewStore as bU, evaluatePolicy as bV, notesSince as bW, sanitizePolicy as bX, staticPolicySource as bY, supabasePolicySource as bZ, useBlockReload as b_, type EvaluatePolicyOptions as ba, type ForceUpgradeProps as bb, ForceUpgradeScreen as bc, type MaintenanceProps as bd, MaintenanceScreen as be, type PolicyClientLike as bf, type PolicyOrigin as bg, type PolicyState as bh, type PolicyStore as bi, type PolicyStoreOptions as bj, type ReleaseNote as bk, type ReloadGuard as bl, type SupabasePolicySourceOptions as bm, UPDATES_BLOCK_ID as bn, UpdateBanner as bo, type UpdateBannerProps as bp, type UpdateCheckState as bq, type UpdateCheckStatus as br, type UpdateChecker as bs, type UpdateCheckerOptions as bt, type UpdatePlatform as bu, type UpdatePolicy as bv, type UpdatePolicySource as bw, type UpdateVerdict as bx, type UpdatesAppStateLike as by, type UpdatesErrorSource as bz, type AuthGateProps as c, useForceUpgrade as c0, useMaintenance as c1, useUpdateBanner as c2, useUpdateCheck as c3, useUpdatePolicy as c4, useUpdateVerdict as c5, useUpdates as c6, useVersionSignature as c7, useWhatsNew as c8, versionSignature as c9, type BootController as d, type BootOptions as e, BootProvider as f, BootScreen as g, type BootScreenProps as h, type BootState as i, type BootStatus as j, type BootStep as k, type BootStepContext as l, BootTimeoutError as m, type ErrorReport as n, type ErrorUtilsLike as o, type FatalScreenProps as p, type GenesisErrorBoundaryProps as q, GenesisNativeApp as r, type GenesisNativeAppProps as s, type GlobalErrorHandlerOptions as t, type LinkOptions as u, type LinkTarget as v, type NativeAppSlots as w, type NativeBlockRootProps as x, type NativeQueryClientOptions as y, type SessionState as z };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/push/payload.ts
|
|
2
|
+
var RICH_PUSH_IMAGE_FIELDS = ["image", "senderAvatar"];
|
|
3
|
+
function isRichPushUrl(value) {
|
|
4
|
+
if (typeof value !== "string" || value.length > 1024) return false;
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(value);
|
|
7
|
+
return url.protocol === "https:" && url.hostname.length > 0 && !url.username && !url.password;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function richPushMessage(message, rich = {}) {
|
|
13
|
+
const data = { ...message.data ?? {} };
|
|
14
|
+
for (const key of RICH_PUSH_IMAGE_FIELDS) {
|
|
15
|
+
if (key in rich) data[key] = rich[key];
|
|
16
|
+
if (key in data && !isRichPushUrl(data[key])) delete data[key];
|
|
17
|
+
}
|
|
18
|
+
return { ...message, mutableContent: true, data };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { RICH_PUSH_IMAGE_FIELDS, isRichPushUrl, richPushMessage };
|
|
22
|
+
//# sourceMappingURL=chunk-ONAXQHNG.js.map
|
|
23
|
+
//# sourceMappingURL=chunk-ONAXQHNG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/push/payload.ts"],"names":[],"mappings":";AA2BO,IAAM,sBAAA,GAAyB,CAAC,OAAA,EAAS,cAAc;AAOvD,SAAS,cAAc,KAAA,EAAiC;AAC7D,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,CAAM,MAAA,GAAS,MAAM,OAAO,KAAA;AAC7D,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,KAAK,CAAA;AACzB,IAAA,OAAO,GAAA,CAAI,QAAA,KAAa,QAAA,IAAY,GAAA,CAAI,QAAA,CAAS,MAAA,GAAS,CAAA,IAAK,CAAC,GAAA,CAAI,QAAA,IAAY,CAAC,GAAA,CAAI,QAAA;AAAA,EACvF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAQO,SAAS,eAAA,CACd,OAAA,EACA,IAAA,GAAqB,EAAC,EACwE;AAC9F,EAAA,MAAM,OAAgC,EAAE,GAAI,OAAA,CAAQ,IAAA,IAAQ,EAAC,EAAG;AAChE,EAAA,KAAA,MAAW,OAAO,sBAAA,EAAwB;AACxC,IAAA,IAAI,OAAO,IAAA,EAAM,IAAA,CAAK,GAAG,CAAA,GAAI,KAAK,GAAG,CAAA;AACrC,IAAA,IAAI,GAAA,IAAO,IAAA,IAAQ,CAAC,aAAA,CAAc,IAAA,CAAK,GAAG,CAAC,CAAA,EAAG,OAAO,IAAA,CAAK,GAAG,CAAA;AAAA,EAC/D;AACA,EAAA,OAAO,EAAE,GAAG,OAAA,EAAS,cAAA,EAAgB,MAAM,IAAA,EAAK;AAClD","file":"chunk-ONAXQHNG.js","sourcesContent":["/**\n * Rich push payloads: what a sender adds to an Expo push message so the iOS\n * Notification Service Extension (`@marktiderman/genesis-native/rich-push-plugin`)\n * runs on it. No React, no React Native, no Node APIs: a server, an edge\n * function or Deno imports it from `@marktiderman/genesis-native/push-payload`.\n *\n * await expo.send([richPushMessage({ to, title, body, data: { url } }, { image })]);\n *\n * iOS runs the extension only when the push says `mutable-content: 1`, which\n * Expo's push service sets from `mutableContent: true`. Without it the\n * notification arrives as sent and the extension never sees it.\n */\n\n/** The image fields the extension reads from `data`. */\nexport interface RichPushData {\n /** An https image to attach (a photo in the message). Anything but https is dropped. */\n image?: string | null | undefined;\n /** The sender's photo, https. On a `messages` push it is the sender's avatar; on any other it is attached. */\n senderAvatar?: string | null | undefined;\n}\n\n/** The part of an Expo push message this touches. */\nexport interface RichPushMessageLike {\n data?: Record<string, unknown> | undefined;\n mutableContent?: boolean | undefined;\n}\n\nexport const RICH_PUSH_IMAGE_FIELDS = [\"image\", \"senderAvatar\"] as const;\n\n/**\n * The extension downloads only https URLs (no credentials, at most 1024\n * characters: APNs takes 4 KB in all); this is the same check. The plugin's\n * `imageHosts`, when set, narrows it on the phone.\n */\nexport function isRichPushUrl(value: unknown): value is string {\n if (typeof value !== \"string\" || value.length > 1024) return false;\n try {\n const url = new URL(value);\n return url.protocol === \"https:\" && url.hostname.length > 0 && !url.username && !url.password;\n } catch {\n return false;\n }\n}\n\n/**\n * The message with `mutableContent: true` and the image fields in its `data`.\n * A field given in `rich` replaces the one in `data`; one that isn't an https\n * URL is left out (the extension would ignore it). `data`'s other fields are\n * kept as they are.\n */\nexport function richPushMessage<M extends RichPushMessageLike>(\n message: M,\n rich: RichPushData = {},\n): Omit<M, \"mutableContent\" | \"data\"> & { mutableContent: true; data: Record<string, unknown> } {\n const data: Record<string, unknown> = { ...(message.data ?? {}) };\n for (const key of RICH_PUSH_IMAGE_FIELDS) {\n if (key in rich) data[key] = rich[key];\n if (key in data && !isRichPushUrl(data[key])) delete data[key];\n }\n return { ...message, mutableContent: true, data };\n}\n"]}
|
package/dist/devtools.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { N as NativeBlock, U as UpdatesBlock, V as VersionSignature } from './blocks-
|
|
2
|
-
export { D as DevtoolsFeedbackProps, a as DevtoolsSlots } from './blocks-
|
|
3
|
-
import { O as ObservabilityBlock, R as RecentActivity } from './block-
|
|
1
|
+
import { N as NativeBlock, U as UpdatesBlock, V as VersionSignature } from './blocks-C2RDpvaN.js';
|
|
2
|
+
export { D as DevtoolsFeedbackProps, a as DevtoolsSlots } from './blocks-C2RDpvaN.js';
|
|
3
|
+
import { O as ObservabilityBlock, R as RecentActivity } from './block-Onqx7NiK.js';
|
|
4
4
|
import 'react';
|
|
5
5
|
import '@marktiderman/genesis-core/slots';
|
|
6
6
|
import '@marktiderman/genesis-features/blocks';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { A as AuthClientLike, b as AuthGate, c as AuthGateProps, B as BlockSettingsSection, d as BootController, e as BootOptions, f as BootProvider, g as BootScreen, h as BootScreenProps, i as BootState, j as BootStatus, k as BootStep, l as BootStepContext, m as BootTimeoutError, E as EnableRejectionTracker, n as ErrorReport, o as ErrorUtilsLike, F as FatalScreen, p as FatalScreenProps, G as GenesisErrorBoundary, q as GenesisErrorBoundaryProps, r as GenesisNativeApp, s as GenesisNativeAppProps, t as GlobalErrorHandlerOptions, H as HeldLinkAuth, L as LinkHold, u as LinkOptions, v as LinkTarget, w as NativeAppSlots, N as NativeBlock, x as NativeBlockRootProps, y as NativeQueryClientOptions, O as OnError, P as PlainFatalScreen, S as SessionProvider, z as SessionState, C as SessionStatus, I as SessionStore, J as SessionStoreOptions, K as SettingsRowLike, M as SignedOutProps, Q as SplashModule, R as UseBlockSettingsOptions, T as UseBootReturn, W as UseHeldLinkOptions, X as createBoot, Y as createLinkHold, Z as createSessionStore, _ as defaultErrorUtils, $ as defaultRejectionTracker, a0 as installGlobalErrorHandler, a1 as installRejectionTracker, a2 as mergeBlockSettings, a3 as normalizeLink, a4 as useBlockSettings, a5 as useBlocks, a6 as useBoot, a7 as useHeldLink, a8 as useSession } from './blocks-
|
|
1
|
+
export { A as AuthClientLike, b as AuthGate, c as AuthGateProps, B as BlockSettingsSection, d as BootController, e as BootOptions, f as BootProvider, g as BootScreen, h as BootScreenProps, i as BootState, j as BootStatus, k as BootStep, l as BootStepContext, m as BootTimeoutError, E as EnableRejectionTracker, n as ErrorReport, o as ErrorUtilsLike, F as FatalScreen, p as FatalScreenProps, G as GenesisErrorBoundary, q as GenesisErrorBoundaryProps, r as GenesisNativeApp, s as GenesisNativeAppProps, t as GlobalErrorHandlerOptions, H as HeldLinkAuth, L as LinkHold, u as LinkOptions, v as LinkTarget, w as NativeAppSlots, N as NativeBlock, x as NativeBlockRootProps, y as NativeQueryClientOptions, O as OnError, P as PlainFatalScreen, S as SessionProvider, z as SessionState, C as SessionStatus, I as SessionStore, J as SessionStoreOptions, K as SettingsRowLike, M as SignedOutProps, Q as SplashModule, R as UseBlockSettingsOptions, T as UseBootReturn, W as UseHeldLinkOptions, X as createBoot, Y as createLinkHold, Z as createSessionStore, _ as defaultErrorUtils, $ as defaultRejectionTracker, a0 as installGlobalErrorHandler, a1 as installRejectionTracker, a2 as mergeBlockSettings, a3 as normalizeLink, a4 as useBlockSettings, a5 as useBlocks, a6 as useBoot, a7 as useHeldLink, a8 as useSession } from './blocks-C2RDpvaN.js';
|
|
2
2
|
import { A as AsyncStorageLike } from './query-DUmVAfsF.js';
|
|
3
3
|
export { a as AppStateLike, D as DAY_MS, N as NetInfoLike, Q as QueryLifecycleOptions, b as QueryPersistence, c as connectQueryLifecycle, d as createNativeQueryClient } from './query-DUmVAfsF.js';
|
|
4
4
|
import 'react';
|
package/dist/observability.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as ObservabilitySink } from './block-
|
|
2
|
-
export { B as Breadcrumb, b as BreadcrumbLevel, C as ConsoleSinkOptions, E as ErrorContext, c as EventCheck, d as EventPropSpec, e as EventProps, f as EventSchema, g as EventValues, h as OBSERVABILITY_BLOCK_ID, O as ObservabilityBlock, i as ObservabilityOptions, j as ObservabilityProvider, k as ObserveLike, l as ObserveSinkOptions, R as RecentActivity, m as RecentError, n as RecentStore, T as TimingTags, o as checkEvent, p as consoleSink, q as createObservability, r as defineEvents, s as observeSink, t as scrubBreadcrumb, u as scrubbedError, v as useObservability, w as useRecentActivity } from './block-
|
|
1
|
+
import { a as ObservabilitySink } from './block-Onqx7NiK.js';
|
|
2
|
+
export { B as Breadcrumb, b as BreadcrumbLevel, C as ConsoleSinkOptions, E as ErrorContext, c as EventCheck, d as EventPropSpec, e as EventProps, f as EventSchema, g as EventValues, h as OBSERVABILITY_BLOCK_ID, O as ObservabilityBlock, i as ObservabilityOptions, j as ObservabilityProvider, k as ObserveLike, l as ObserveSinkOptions, R as RecentActivity, m as RecentError, n as RecentStore, T as TimingTags, o as checkEvent, p as consoleSink, q as createObservability, r as defineEvents, s as observeSink, t as scrubBreadcrumb, u as scrubbedError, v as useObservability, w as useRecentActivity } from './block-Onqx7NiK.js';
|
|
3
3
|
import { ComponentType } from 'react';
|
|
4
|
-
import { V as VersionSignature } from './blocks-
|
|
4
|
+
import { V as VersionSignature } from './blocks-C2RDpvaN.js';
|
|
5
5
|
import '@marktiderman/genesis-core/slots';
|
|
6
6
|
import '@marktiderman/genesis-features/blocks';
|
|
7
7
|
import '@tanstack/react-query';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rich push payloads: what a sender adds to an Expo push message so the iOS
|
|
3
|
+
* Notification Service Extension (`@marktiderman/genesis-native/rich-push-plugin`)
|
|
4
|
+
* runs on it. No React, no React Native, no Node APIs: a server, an edge
|
|
5
|
+
* function or Deno imports it from `@marktiderman/genesis-native/push-payload`.
|
|
6
|
+
*
|
|
7
|
+
* await expo.send([richPushMessage({ to, title, body, data: { url } }, { image })]);
|
|
8
|
+
*
|
|
9
|
+
* iOS runs the extension only when the push says `mutable-content: 1`, which
|
|
10
|
+
* Expo's push service sets from `mutableContent: true`. Without it the
|
|
11
|
+
* notification arrives as sent and the extension never sees it.
|
|
12
|
+
*/
|
|
13
|
+
/** The image fields the extension reads from `data`. */
|
|
14
|
+
interface RichPushData {
|
|
15
|
+
/** An https image to attach (a photo in the message). Anything but https is dropped. */
|
|
16
|
+
image?: string | null | undefined;
|
|
17
|
+
/** The sender's photo, https. On a `messages` push it is the sender's avatar; on any other it is attached. */
|
|
18
|
+
senderAvatar?: string | null | undefined;
|
|
19
|
+
}
|
|
20
|
+
/** The part of an Expo push message this touches. */
|
|
21
|
+
interface RichPushMessageLike {
|
|
22
|
+
data?: Record<string, unknown> | undefined;
|
|
23
|
+
mutableContent?: boolean | undefined;
|
|
24
|
+
}
|
|
25
|
+
declare const RICH_PUSH_IMAGE_FIELDS: readonly ["image", "senderAvatar"];
|
|
26
|
+
/**
|
|
27
|
+
* The extension downloads only https URLs (no credentials, at most 1024
|
|
28
|
+
* characters: APNs takes 4 KB in all); this is the same check. The plugin's
|
|
29
|
+
* `imageHosts`, when set, narrows it on the phone.
|
|
30
|
+
*/
|
|
31
|
+
declare function isRichPushUrl(value: unknown): value is string;
|
|
32
|
+
/**
|
|
33
|
+
* The message with `mutableContent: true` and the image fields in its `data`.
|
|
34
|
+
* A field given in `rich` replaces the one in `data`; one that isn't an https
|
|
35
|
+
* URL is left out (the extension would ignore it). `data`'s other fields are
|
|
36
|
+
* kept as they are.
|
|
37
|
+
*/
|
|
38
|
+
declare function richPushMessage<M extends RichPushMessageLike>(message: M, rich?: RichPushData): Omit<M, "mutableContent" | "data"> & {
|
|
39
|
+
mutableContent: true;
|
|
40
|
+
data: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { RICH_PUSH_IMAGE_FIELDS, type RichPushData, type RichPushMessageLike, isRichPushUrl, richPushMessage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"push-payload.js"}
|
package/dist/push.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { a9 as PushPermissionStatus, aa as
|
|
2
|
-
export { ad as ChannelImportance, ae as ChannelInputLike, af as DEFAULT_PREFERENCES, ag as
|
|
1
|
+
import { a9 as PushPermissionStatus, aa as NotificationResponseLike, ab as NotificationLike, u as LinkOptions, ac as QuietHours } from './blocks-C2RDpvaN.js';
|
|
2
|
+
export { ad as ChannelImportance, ae as ChannelInputLike, af as DEFAULT_PREFERENCES, ag as NotificationActionLike, ah as NotificationBehaviorLike, ai as NotificationCategory, aj as NotificationPreferences, ak as NotificationsModuleLike, al as PermissionRequestLike, am as PermissionState, an as PermissionStatusLike, ao as PermissionStore, ap as PreferencesState, aq as PreferencesStatus, ar as PreferencesStore, as as PromptOptions, at as PromptState, au as PromptStore, av as PushAppStateLike, aw as PushBackend, ax as PushBackendError, ay as PushBlock, az as PushChannel, aA as PushDeviceCategory, aB as PushErrorSource, aC as PushOnError, aD as PushOptions, aE as PushPromptProps, aF as PushPromptSheet, aG as PushProvider, aH as PushRoot, aI as PushRootProps, aJ as PushSlots, aK as PushStorageLike, aL as PushSupabaseLike, aM as Registration, aN as RegistrationState, aO as RegistrationStatus, aP as SubscriptionLike, aQ as canPush, aR as createPermissionStore, aS as createPreferencesStore, aT as createPromptStore, aU as createPush, aV as createRegistration, aW as statusOf, aX as supabasePushBackend, aY as useBadge, aZ as useNotificationPreferences, a_ as useOptionalPush, a$ as usePush, b0 as usePushPermissionState, b1 as usePushPrompt, b2 as usePushRegistration, b3 as useSuppressNotifications, b4 as validPreferences } from './blocks-C2RDpvaN.js';
|
|
3
3
|
import * as react from 'react';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
5
5
|
import { Slots, SlotPropsFor } from '@marktiderman/genesis-core/slots';
|
|
6
6
|
import { SettingsPageProps, SettingsPageRow } from '@marktiderman/genesis-ui-native/layouts';
|
|
7
|
+
export { RICH_PUSH_IMAGE_FIELDS, RichPushData, RichPushMessageLike, isRichPushUrl, richPushMessage } from './push-payload.js';
|
|
7
8
|
import '@marktiderman/genesis-features/blocks';
|
|
8
9
|
import '@tanstack/react-query';
|
|
9
10
|
import '@marktiderman/genesis-design-system';
|
|
@@ -139,6 +140,23 @@ declare function NotificationSettings({ copy: overrides, slots, slotProps, page,
|
|
|
139
140
|
declare function notificationData(notification: NotificationLike): Record<string, unknown>;
|
|
140
141
|
/** The app path a tap should open, or null. */
|
|
141
142
|
declare function pathForResponse(response: NotificationResponseLike, links?: LinkOptions): string | null;
|
|
143
|
+
/** A notification action (a button such as Reply or Mark as read), as the app handles it. */
|
|
144
|
+
interface NotificationAction {
|
|
145
|
+
/** The button's id (`reply`, `mark_read`); the plain tap is not an action. */
|
|
146
|
+
actionId: string;
|
|
147
|
+
/** The notifier's id for the notification (`data.notification_id`): use it as the reply's client id, so a retried reply stays one reply. */
|
|
148
|
+
notificationId: string | null;
|
|
149
|
+
/** What the person typed, for a text-input action. */
|
|
150
|
+
text: string | null;
|
|
151
|
+
/** `data.url`, the screen it belongs to (`/messages/<conversation>`). */
|
|
152
|
+
url: string | null;
|
|
153
|
+
data: Record<string, unknown>;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The action in a response, or null for a plain tap (`defaultActionId`,
|
|
157
|
+
* expo-notifications' `DEFAULT_ACTION_IDENTIFIER`) or a dismissal.
|
|
158
|
+
*/
|
|
159
|
+
declare function notificationAction(response: NotificationResponseLike, defaultActionId?: string): NotificationAction | null;
|
|
142
160
|
|
|
143
161
|
/**
|
|
144
162
|
* Quiet hours on the phone: only for showing them ("on now, until 07:00").
|
|
@@ -166,4 +184,4 @@ declare function minutesIn(timeZone: string, at: Date): number | null;
|
|
|
166
184
|
*/
|
|
167
185
|
declare function isQuietAt(quiet: QuietHours, at?: Date): boolean;
|
|
168
186
|
|
|
169
|
-
export { type CategoriesPartProps, type CategoryRow, NotificationLike, NotificationResponseLike, NotificationSettings, type NotificationSettingsCopy, type NotificationSettingsProps, type NotificationSettingsSlots, type PermissionPartProps, type PrivacyPartProps, PushPermissionStatus, QuietHours, type QuietHoursPartProps, categoryRows, clockOf, dateAt, deviceTimeZone, formatClock, isQuietAt, minutesIn, notificationData, notificationSettingsCopy, parseClock, pathForResponse, permissionRows, privacyRows, quietHoursRows, useNotificationCategories, usePrivacyPreviews, usePushPermission, useQuietHours };
|
|
187
|
+
export { type CategoriesPartProps, type CategoryRow, type NotificationAction, NotificationLike, NotificationResponseLike, NotificationSettings, type NotificationSettingsCopy, type NotificationSettingsProps, type NotificationSettingsSlots, type PermissionPartProps, type PrivacyPartProps, PushPermissionStatus, QuietHours, type QuietHoursPartProps, categoryRows, clockOf, dateAt, deviceTimeZone, formatClock, isQuietAt, minutesIn, notificationAction, notificationData, notificationSettingsCopy, parseClock, pathForResponse, permissionRows, privacyRows, quietHoursRows, useNotificationCategories, usePrivacyPreviews, usePushPermission, useQuietHours };
|
package/dist/push.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeLink } from './chunk-VKTIHNAV.js';
|
|
2
2
|
import { createStore, delay } from './chunk-XE7LC7K6.js';
|
|
3
|
+
export { RICH_PUSH_IMAGE_FIELDS, isRichPushUrl, richPushMessage } from './chunk-ONAXQHNG.js';
|
|
3
4
|
import { useSession } from './chunk-NGYNYJPL.js';
|
|
4
5
|
import { createContext, useSyncExternalStore, useContext, useRef, useEffect, useMemo } from 'react';
|
|
5
6
|
import { View } from 'react-native';
|
|
@@ -321,11 +322,28 @@ function createPreferencesStore(options) {
|
|
|
321
322
|
}
|
|
322
323
|
|
|
323
324
|
// src/push/registration.ts
|
|
325
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
326
|
+
function makeUUID(maker) {
|
|
327
|
+
try {
|
|
328
|
+
const made = maker?.() ?? globalThis.crypto?.randomUUID?.();
|
|
329
|
+
if (typeof made === "string" && UUID.test(made)) return made.toLowerCase();
|
|
330
|
+
const bytes = globalThis.crypto?.getRandomValues?.(new Uint8Array(16));
|
|
331
|
+
if (!bytes) return void 0;
|
|
332
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
333
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
334
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
335
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
336
|
+
} catch {
|
|
337
|
+
return void 0;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
324
340
|
var REFRESH_MS = 60 * 6e4;
|
|
325
341
|
function createRegistration(options) {
|
|
326
342
|
const { notifications, backend, storage, storageKeyPrefix, onError, now } = options;
|
|
327
343
|
const savedKey = `${storageKeyPrefix}:device`;
|
|
328
344
|
const pendingKey = `${storageKeyPrefix}:forget`;
|
|
345
|
+
const installationKey = `${storageKeyPrefix}:installation`;
|
|
346
|
+
let installation = null;
|
|
329
347
|
const store = createStore({ status: "idle", problem: null });
|
|
330
348
|
let current = null;
|
|
331
349
|
let queue = Promise.resolve();
|
|
@@ -354,6 +372,16 @@ function createRegistration(options) {
|
|
|
354
372
|
report(onError, error, "storage");
|
|
355
373
|
}
|
|
356
374
|
};
|
|
375
|
+
const installationId = async () => {
|
|
376
|
+
if (installation) return installation;
|
|
377
|
+
const saved = await read(installationKey, null);
|
|
378
|
+
if (typeof saved === "string" && UUID.test(saved)) return installation = saved;
|
|
379
|
+
const made = makeUUID(options.randomUUID);
|
|
380
|
+
if (!made) return void 0;
|
|
381
|
+
installation = made;
|
|
382
|
+
await write(installationKey, made);
|
|
383
|
+
return made;
|
|
384
|
+
};
|
|
357
385
|
const forgetToken = async (token) => {
|
|
358
386
|
try {
|
|
359
387
|
await backend.forget(token);
|
|
@@ -404,7 +432,9 @@ function createRegistration(options) {
|
|
|
404
432
|
}
|
|
405
433
|
if (!options.isCurrent(userId)) return;
|
|
406
434
|
try {
|
|
407
|
-
|
|
435
|
+
const platform = options.platform === "ios" ? "ios" : "android";
|
|
436
|
+
if (backend.usesInstallationId) await backend.register(token, platform, options.appVersion ?? null, await installationId());
|
|
437
|
+
else await backend.register(token, platform, options.appVersion ?? null);
|
|
408
438
|
} catch (error) {
|
|
409
439
|
report(onError, error, "register");
|
|
410
440
|
store.setState({ status: "error", problem: "backend" });
|
|
@@ -457,6 +487,18 @@ function pathForResponse(response, links) {
|
|
|
457
487
|
const path = normalizeLink(url, ANY_PATH);
|
|
458
488
|
return path === "/" && url.split(/[?#]/)[0] !== "/" ? null : path;
|
|
459
489
|
}
|
|
490
|
+
function notificationAction(response, defaultActionId = "expo.modules.notifications.actions.DEFAULT") {
|
|
491
|
+
const id = response.actionIdentifier;
|
|
492
|
+
if (!id || id === defaultActionId || id === "com.apple.UNNotificationDismissActionIdentifier") return null;
|
|
493
|
+
const data = notificationData(response.notification);
|
|
494
|
+
return {
|
|
495
|
+
actionId: id,
|
|
496
|
+
notificationId: typeof data.notification_id === "string" ? data.notification_id : null,
|
|
497
|
+
text: typeof response.userText === "string" ? response.userText : null,
|
|
498
|
+
url: typeof data.url === "string" ? data.url : null,
|
|
499
|
+
data
|
|
500
|
+
};
|
|
501
|
+
}
|
|
460
502
|
var PROMPT_CONTENT = { flexGrow: 1, justifyContent: "center", gap: 16, padding: 24 };
|
|
461
503
|
function PushPromptSheet({ reason, accept, decline, title, body, acceptLabel, declineLabel }) {
|
|
462
504
|
return /* @__PURE__ */ jsx(NativeSheet, { open: true, onOpenChange: (open) => open ? void 0 : decline(), testID: "genesis-push-prompt", children: /* @__PURE__ */ jsxs(View, { style: PROMPT_CONTENT, testID: "genesis-push-prompt-content", children: [
|
|
@@ -508,6 +550,30 @@ function createPush(options) {
|
|
|
508
550
|
});
|
|
509
551
|
return channelsMade;
|
|
510
552
|
};
|
|
553
|
+
let categoriesMade = false;
|
|
554
|
+
const registerDeviceCategories = async () => {
|
|
555
|
+
const set = notifications?.setNotificationCategoryAsync;
|
|
556
|
+
if (categoriesMade || !notifications || !set || !options.deviceCategories?.length) return;
|
|
557
|
+
categoriesMade = true;
|
|
558
|
+
for (const c of options.deviceCategories) {
|
|
559
|
+
try {
|
|
560
|
+
await set.call(
|
|
561
|
+
notifications,
|
|
562
|
+
c.identifier,
|
|
563
|
+
c.actions.map((a) => ({
|
|
564
|
+
identifier: a.id,
|
|
565
|
+
buttonTitle: a.title,
|
|
566
|
+
...a.textInput ? { textInput: { placeholder: a.textInput.placeholder ?? "", submitButtonTitle: a.textInput.submitTitle ?? a.title } } : {},
|
|
567
|
+
options: { opensAppToForeground: a.opensApp ?? false, isDestructive: a.destructive ?? false, isAuthenticationRequired: a.requiresAuth ?? false }
|
|
568
|
+
})),
|
|
569
|
+
c.previewPlaceholder ? { previewPlaceholder: c.previewPlaceholder } : void 0
|
|
570
|
+
);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
categoriesMade = false;
|
|
573
|
+
report(onError, error, "categories");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
};
|
|
511
577
|
const permission = createPermissionStore({ notifications, request: options.permissions, beforeRequest: ensureChannels, onError });
|
|
512
578
|
const registration = createRegistration({
|
|
513
579
|
notifications,
|
|
@@ -520,6 +586,7 @@ function createPush(options) {
|
|
|
520
586
|
ensureChannels,
|
|
521
587
|
isCurrent: (id) => userId === id,
|
|
522
588
|
now,
|
|
589
|
+
randomUUID: options.randomUUID,
|
|
523
590
|
onError
|
|
524
591
|
});
|
|
525
592
|
const preferences = createPreferencesStore({ backend: options.backend, categories, onError });
|
|
@@ -637,8 +704,14 @@ function createPush(options) {
|
|
|
637
704
|
});
|
|
638
705
|
void registration.flush();
|
|
639
706
|
void permission.refresh();
|
|
640
|
-
|
|
641
|
-
|
|
707
|
+
void registerDeviceCategories();
|
|
708
|
+
let deviceToken;
|
|
709
|
+
const tokenSubscription = notifications.addPushTokenListener((token) => {
|
|
710
|
+
const next = token?.data;
|
|
711
|
+
const seen = deviceToken;
|
|
712
|
+
deviceToken = next;
|
|
713
|
+
if (!userId || next === seen) return;
|
|
714
|
+
void registration.sync(userId, { force: seen !== void 0 });
|
|
642
715
|
});
|
|
643
716
|
const appSubscription = options.appState?.addEventListener("change", (state) => {
|
|
644
717
|
if (state !== "active") return;
|
|
@@ -991,6 +1064,6 @@ function NotificationSettings({ copy: overrides, slots = {}, slotProps = {}, pag
|
|
|
991
1064
|
return /* @__PURE__ */ jsx(SettingsPage, { testID, ...page, sections, children });
|
|
992
1065
|
}
|
|
993
1066
|
|
|
994
|
-
export { DEFAULT_PREFERENCES, NotificationSettings, PushBackendError, PushPromptSheet, PushProvider, PushRoot, canPush, categoryRows, clockOf, createPermissionStore, createPreferencesStore, createPromptStore, createPush, createRegistration, dateAt, deviceTimeZone, formatClock, isQuietAt, minutesIn, notificationData, notificationSettingsCopy, parseClock, pathForResponse, permissionRows, privacyRows, quietHoursRows, statusOf, supabasePushBackend, useBadge, useNotificationCategories, useNotificationPreferences, useOptionalPush, usePrivacyPreviews, usePush, usePushPermission, usePushPermissionState, usePushPrompt, usePushRegistration, useQuietHours, useSuppressNotifications, validPreferences };
|
|
1067
|
+
export { DEFAULT_PREFERENCES, NotificationSettings, PushBackendError, PushPromptSheet, PushProvider, PushRoot, canPush, categoryRows, clockOf, createPermissionStore, createPreferencesStore, createPromptStore, createPush, createRegistration, dateAt, deviceTimeZone, formatClock, isQuietAt, minutesIn, notificationAction, notificationData, notificationSettingsCopy, parseClock, pathForResponse, permissionRows, privacyRows, quietHoursRows, statusOf, supabasePushBackend, useBadge, useNotificationCategories, useNotificationPreferences, useOptionalPush, usePrivacyPreviews, usePush, usePushPermission, usePushPermissionState, usePushPrompt, usePushRegistration, useQuietHours, useSuppressNotifications, validPreferences };
|
|
995
1068
|
//# sourceMappingURL=push.js.map
|
|
996
1069
|
//# sourceMappingURL=push.js.map
|