@marktiderman/genesis-native 0.2.0 → 0.4.0
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 +470 -10
- package/dist/block-Onqx7NiK.d.ts +223 -0
- package/dist/blocks-C2RDpvaN.d.ts +1595 -0
- package/dist/chunk-65P7RU4V.js +49 -0
- package/dist/chunk-65P7RU4V.js.map +1 -0
- package/dist/chunk-6BT4PTGH.js +37 -0
- package/dist/chunk-6BT4PTGH.js.map +1 -0
- package/dist/chunk-75YP72G6.js +374 -0
- package/dist/chunk-75YP72G6.js.map +1 -0
- package/dist/chunk-NGYNYJPL.js +90 -0
- package/dist/chunk-NGYNYJPL.js.map +1 -0
- package/dist/chunk-ONAXQHNG.js +23 -0
- package/dist/chunk-ONAXQHNG.js.map +1 -0
- package/dist/chunk-VKTIHNAV.js +95 -0
- package/dist/chunk-VKTIHNAV.js.map +1 -0
- package/dist/{chunk-MMTPWUWI.js → chunk-VTJDIKFC.js} +9 -6
- package/dist/chunk-VTJDIKFC.js.map +1 -0
- package/dist/chunk-XE7LC7K6.js +29 -0
- package/dist/chunk-XE7LC7K6.js.map +1 -0
- package/dist/dev-warnings.d.ts +34 -0
- package/dist/dev-warnings.js +15 -0
- package/dist/dev-warnings.js.map +1 -0
- package/dist/devtools.d.ts +144 -0
- package/dist/devtools.js +390 -0
- package/dist/devtools.js.map +1 -0
- package/dist/index.d.ts +12 -368
- package/dist/index.js +88 -191
- package/dist/index.js.map +1 -1
- package/dist/observability.d.ts +170 -0
- package/dist/observability.js +123 -0
- package/dist/observability.js.map +1 -0
- package/dist/persist.d.ts +1 -1
- package/dist/persist.js +1 -1
- 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 +187 -0
- package/dist/push.js +1069 -0
- package/dist/push.js.map +1 -0
- package/dist/{query-DhH3lLFa.d.ts → query-DUmVAfsF.d.ts} +11 -6
- 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 +8 -1
- package/dist/updates.js +44 -69
- package/dist/updates.js.map +1 -1
- package/ios/notification-service/NotificationService.swift +309 -0
- package/package.json +238 -5
- package/src/app.tsx +491 -0
- package/src/auth.tsx +176 -0
- package/src/blocks.tsx +144 -0
- package/src/boot.tsx +188 -0
- package/src/dev-warnings.ts +43 -0
- package/src/devtools/block.tsx +488 -0
- package/src/devtools/gestures.ts +107 -0
- package/src/devtools/index.ts +22 -0
- package/src/devtools/info.ts +45 -0
- package/src/devtools/slots.ts +21 -0
- package/src/errors.tsx +195 -0
- package/src/held-link.ts +90 -0
- package/src/hydration-gate.ts +88 -0
- package/src/index.ts +83 -0
- package/src/links.ts +140 -0
- package/src/nativewind.d.ts +41 -0
- package/src/observability/block.tsx +300 -0
- package/src/observability/events.ts +89 -0
- package/src/observability/index.ts +44 -0
- package/src/observability/scrub.ts +166 -0
- package/src/observability/sentry.ts +222 -0
- package/src/observability/sink.ts +147 -0
- package/src/persist.ts +83 -0
- package/src/plugin/rich-push.ts +312 -0
- package/src/push/backend.ts +137 -0
- package/src/push/block.tsx +604 -0
- package/src/push/index.ts +105 -0
- package/src/push/modules.ts +123 -0
- package/src/push/payload.ts +61 -0
- package/src/push/permission.ts +191 -0
- package/src/push/preferences.ts +136 -0
- package/src/push/quiet-hours.ts +71 -0
- package/src/push/registration.ts +225 -0
- package/src/push/routing.ts +57 -0
- package/src/push/screens.tsx +46 -0
- package/src/push/settings.tsx +378 -0
- package/src/query.ts +100 -0
- package/src/updates/block.tsx +442 -0
- package/src/updates/channels.ts +131 -0
- package/src/updates/checker.ts +190 -0
- package/src/updates/index.ts +87 -0
- package/src/updates/modules.ts +64 -0
- package/src/updates/policy.ts +314 -0
- package/src/updates/screens.tsx +159 -0
- package/src/updates/signature.ts +80 -0
- package/src/updates/store.ts +33 -0
- package/src/updates/version.ts +39 -0
- package/src/updates/whats-new.ts +122 -0
- package/dist/block-Dh5ny06W.d.ts +0 -535
- package/dist/chunk-MMTPWUWI.js.map +0 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Genesis rich push: the Notification Service Extension that
|
|
2
|
+
// `@marktiderman/genesis-native/rich-push-plugin` adds to an Expo app.
|
|
3
|
+
//
|
|
4
|
+
// iOS runs it for a push with `mutable-content: 1` (Expo: `mutableContent: true`).
|
|
5
|
+
// It reads only the payload: it never fetches a message, so a push sent with
|
|
6
|
+
// hidden previews (neutral text, no sender fields) stays neutral.
|
|
7
|
+
//
|
|
8
|
+
// - `data.image` / `data.senderAvatar` (https only, and only from the hosts in
|
|
9
|
+
// the Info.plist's `GenesisRichPushImageHosts` when the plugin's `imageHosts`
|
|
10
|
+
// set it) are downloaded within the time limit and attached. A download that fails or runs late is skipped:
|
|
11
|
+
// the notification is always shown, with what the payload already said.
|
|
12
|
+
// - A push in the `messages` category becomes a Communication Notification:
|
|
13
|
+
// an incoming INSendMessageIntent with the sender (`senderName`,
|
|
14
|
+
// `senderAvatar`, `senderId`), the conversation (`threadId`, else
|
|
15
|
+
// `data.conversationId`) and, in a group, `groupName`. The sender's photo
|
|
16
|
+
// shows on the notification, and Focus treats it as a message.
|
|
17
|
+
//
|
|
18
|
+
// `data` is where Expo's push service puts it (`body` in the APNs payload),
|
|
19
|
+
// or the payload's top level for a push sent to APNs directly.
|
|
20
|
+
//
|
|
21
|
+
// Nothing here logs what a notification says.
|
|
22
|
+
|
|
23
|
+
import Intents
|
|
24
|
+
import UserNotifications
|
|
25
|
+
import os
|
|
26
|
+
|
|
27
|
+
private let messagesCategory = "messages"
|
|
28
|
+
private let maxImageBytes = 10 * 1024 * 1024
|
|
29
|
+
// The extension has about 24 MB; only the avatar is read into memory, and small.
|
|
30
|
+
private let maxAvatarBytes = 1024 * 1024
|
|
31
|
+
private let maxURLLength = 1024
|
|
32
|
+
private let log = Logger(subsystem: "genesis.rich-push", category: "service")
|
|
33
|
+
|
|
34
|
+
final class NotificationService: UNNotificationServiceExtension {
|
|
35
|
+
private let lock = NSLock()
|
|
36
|
+
private var contentHandler: ((UNNotificationContent) -> Void)?
|
|
37
|
+
private var bestAttempt: UNNotificationContent?
|
|
38
|
+
private var session: URLSession?
|
|
39
|
+
|
|
40
|
+
override func didReceive(
|
|
41
|
+
_ request: UNNotificationRequest,
|
|
42
|
+
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
|
43
|
+
) {
|
|
44
|
+
lock.lock()
|
|
45
|
+
self.contentHandler = contentHandler
|
|
46
|
+
self.bestAttempt = request.content
|
|
47
|
+
lock.unlock()
|
|
48
|
+
|
|
49
|
+
guard let content = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
|
50
|
+
deliver(request.content)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
let data = payloadData(request.content.userInfo)
|
|
54
|
+
let isMessage = content.categoryIdentifier == messagesCategory
|
|
55
|
+
let imageURL = httpsURL(data["image"])
|
|
56
|
+
let avatarURL = httpsURL(data["senderAvatar"])
|
|
57
|
+
|
|
58
|
+
if imageURL == nil && avatarURL == nil && !isMessage {
|
|
59
|
+
deliver(content)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
64
|
+
configuration.timeoutIntervalForRequest = 8
|
|
65
|
+
configuration.timeoutIntervalForResource = 15
|
|
66
|
+
configuration.httpCookieStorage = nil
|
|
67
|
+
configuration.urlCache = nil
|
|
68
|
+
let session = URLSession(configuration: configuration)
|
|
69
|
+
lock.lock()
|
|
70
|
+
self.session = session
|
|
71
|
+
lock.unlock()
|
|
72
|
+
|
|
73
|
+
let group = DispatchGroup()
|
|
74
|
+
var imageFile: URL?
|
|
75
|
+
var avatarData: Data?
|
|
76
|
+
var avatarFile: URL?
|
|
77
|
+
if let imageURL {
|
|
78
|
+
group.enter()
|
|
79
|
+
download(imageURL, session: session, keepBytes: false) { file, _ in
|
|
80
|
+
imageFile = file
|
|
81
|
+
group.leave()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if let avatarURL {
|
|
85
|
+
group.enter()
|
|
86
|
+
download(avatarURL, session: session, keepBytes: true) { file, bytes in
|
|
87
|
+
avatarFile = file
|
|
88
|
+
avatarData = bytes
|
|
89
|
+
group.leave()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
group.notify(queue: .global(qos: .userInitiated)) { [weak self] in
|
|
94
|
+
// Already shown (the time ran out): don't donate or build anything more.
|
|
95
|
+
guard let self, !self.delivered else { return }
|
|
96
|
+
var attachments: [UNNotificationAttachment] = []
|
|
97
|
+
if let imageFile, let attachment = self.attachment("image", imageFile) {
|
|
98
|
+
attachments.append(attachment)
|
|
99
|
+
}
|
|
100
|
+
// On a message the avatar is the sender's photo (the intent's); on anything else it is attached.
|
|
101
|
+
if !isMessage, let avatarFile, let attachment = self.attachment("avatar", avatarFile) {
|
|
102
|
+
attachments.append(attachment)
|
|
103
|
+
}
|
|
104
|
+
content.attachments = attachments
|
|
105
|
+
self.keep(content)
|
|
106
|
+
|
|
107
|
+
guard isMessage else {
|
|
108
|
+
self.deliver(content)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
self.communication(content, data: data, avatar: avatarData, avatarFile: avatarFile)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
override func serviceExtensionTimeWillExpire() {
|
|
116
|
+
// Out of time: show what we have (at worst the push as it arrived), never nothing.
|
|
117
|
+
lock.lock()
|
|
118
|
+
let content = bestAttempt
|
|
119
|
+
session?.invalidateAndCancel()
|
|
120
|
+
lock.unlock()
|
|
121
|
+
if let content { deliver(content) }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// MARK: Communication Notifications
|
|
125
|
+
|
|
126
|
+
private func communication(_ content: UNMutableNotificationContent, data: [String: Any], avatar: Data?, avatarFile: URL?) {
|
|
127
|
+
// Not a communication notification after all: the avatar is the picture, as on any other push.
|
|
128
|
+
let plain = { [weak self] in
|
|
129
|
+
guard let self else { return }
|
|
130
|
+
if content.attachments.isEmpty, let avatarFile, let attachment = self.attachment("avatar", avatarFile) {
|
|
131
|
+
content.attachments = [attachment]
|
|
132
|
+
}
|
|
133
|
+
self.deliver(content)
|
|
134
|
+
}
|
|
135
|
+
guard let name = text(data["senderName"]) else {
|
|
136
|
+
// No sender (hidden previews send none): a plain notification, as sent.
|
|
137
|
+
plain()
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
let conversationId = text(content.threadIdentifier) ?? text(data["conversationId"])
|
|
141
|
+
let senderId = text(data["senderId"]) ?? name
|
|
142
|
+
let sender = INPerson(
|
|
143
|
+
personHandle: INPersonHandle(value: senderId, type: .unknown),
|
|
144
|
+
nameComponents: nil,
|
|
145
|
+
displayName: name,
|
|
146
|
+
image: avatar.map { INImage(imageData: $0) },
|
|
147
|
+
contactIdentifier: nil,
|
|
148
|
+
customIdentifier: senderId
|
|
149
|
+
)
|
|
150
|
+
let groupName = text(data["groupName"])
|
|
151
|
+
var recipients: [INPerson]?
|
|
152
|
+
if groupName != nil {
|
|
153
|
+
// iOS draws a group (its name over the sender) when the message has more than one recipient.
|
|
154
|
+
let me = INPerson(
|
|
155
|
+
personHandle: INPersonHandle(value: "me", type: .unknown),
|
|
156
|
+
nameComponents: nil,
|
|
157
|
+
displayName: nil,
|
|
158
|
+
image: nil,
|
|
159
|
+
contactIdentifier: nil,
|
|
160
|
+
customIdentifier: nil,
|
|
161
|
+
isMe: true
|
|
162
|
+
)
|
|
163
|
+
recipients = [me, sender]
|
|
164
|
+
}
|
|
165
|
+
let intent = INSendMessageIntent(
|
|
166
|
+
recipients: recipients,
|
|
167
|
+
outgoingMessageType: .outgoingMessageText,
|
|
168
|
+
content: content.body,
|
|
169
|
+
speakableGroupName: groupName.map { INSpeakableString(spokenPhrase: $0) },
|
|
170
|
+
conversationIdentifier: conversationId,
|
|
171
|
+
serviceName: nil,
|
|
172
|
+
sender: sender,
|
|
173
|
+
attachments: nil
|
|
174
|
+
)
|
|
175
|
+
let interaction = INInteraction(intent: intent, response: nil)
|
|
176
|
+
interaction.direction = .incoming
|
|
177
|
+
interaction.donate { [weak self] error in
|
|
178
|
+
guard let self else { return }
|
|
179
|
+
if error != nil { log.error("donate failed") }
|
|
180
|
+
do {
|
|
181
|
+
let updated = try content.updating(from: intent)
|
|
182
|
+
self.deliver(updated)
|
|
183
|
+
} catch {
|
|
184
|
+
// Without the entitlement (or on an iOS that refuses), the plain notification still shows.
|
|
185
|
+
log.error("communication update failed")
|
|
186
|
+
plain()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// MARK: Delivery
|
|
192
|
+
|
|
193
|
+
private var delivered: Bool {
|
|
194
|
+
lock.lock()
|
|
195
|
+
defer { lock.unlock() }
|
|
196
|
+
return contentHandler == nil
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// What `serviceExtensionTimeWillExpire` shows if the time runs out from here on.
|
|
200
|
+
private func keep(_ content: UNNotificationContent) {
|
|
201
|
+
lock.lock()
|
|
202
|
+
bestAttempt = content.copy() as? UNNotificationContent
|
|
203
|
+
lock.unlock()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private func deliver(_ content: UNNotificationContent) {
|
|
207
|
+
lock.lock()
|
|
208
|
+
let handler = contentHandler
|
|
209
|
+
contentHandler = nil
|
|
210
|
+
session?.finishTasksAndInvalidate()
|
|
211
|
+
session = nil
|
|
212
|
+
lock.unlock()
|
|
213
|
+
handler?(content)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// MARK: Downloads
|
|
217
|
+
|
|
218
|
+
private func download(_ url: URL, session: URLSession, keepBytes: Bool, done: @escaping (URL?, Data?) -> Void) {
|
|
219
|
+
let task = session.downloadTask(with: url) { location, response, error in
|
|
220
|
+
guard error == nil, let location,
|
|
221
|
+
let http = response as? HTTPURLResponse, http.statusCode == 200,
|
|
222
|
+
let mime = http.mimeType?.lowercased(), let ext = imageExtension(mime)
|
|
223
|
+
else {
|
|
224
|
+
log.error("image download skipped")
|
|
225
|
+
done(nil, nil)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
do {
|
|
229
|
+
let size = (try FileManager.default.attributesOfItem(atPath: location.path)[.size] as? NSNumber)?.intValue ?? 0
|
|
230
|
+
guard size > 0, size <= maxImageBytes else {
|
|
231
|
+
done(nil, nil)
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
235
|
+
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
236
|
+
let file = dir.appendingPathComponent("image.\(ext)")
|
|
237
|
+
try FileManager.default.moveItem(at: location, to: file)
|
|
238
|
+
// Bytes only for the avatar (the intent's INImage), and only a small one.
|
|
239
|
+
done(file, keepBytes && size <= maxAvatarBytes ? try? Data(contentsOf: file) : nil)
|
|
240
|
+
} catch {
|
|
241
|
+
log.error("image save failed")
|
|
242
|
+
done(nil, nil)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
task.resume()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private func attachment(_ id: String, _ file: URL) -> UNNotificationAttachment? {
|
|
249
|
+
do {
|
|
250
|
+
return try UNNotificationAttachment(identifier: id, url: file, options: nil)
|
|
251
|
+
} catch {
|
|
252
|
+
log.error("attachment refused")
|
|
253
|
+
return nil
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// MARK: Payload
|
|
259
|
+
|
|
260
|
+
/// `data`: Expo puts it under `body`; a push sent to APNs directly has it at the top level.
|
|
261
|
+
private func payloadData(_ userInfo: [AnyHashable: Any]) -> [String: Any] {
|
|
262
|
+
if let body = userInfo["body"] as? [String: Any] { return body }
|
|
263
|
+
if let body = userInfo["body"] as? String, let bytes = body.data(using: .utf8),
|
|
264
|
+
let parsed = try? JSONSerialization.jsonObject(with: bytes) as? [String: Any]
|
|
265
|
+
{
|
|
266
|
+
return parsed
|
|
267
|
+
}
|
|
268
|
+
var top: [String: Any] = [:]
|
|
269
|
+
for (key, value) in userInfo {
|
|
270
|
+
if let key = key as? String, key != "aps" { top[key] = value }
|
|
271
|
+
}
|
|
272
|
+
return top
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private func text(_ value: Any?) -> String? {
|
|
276
|
+
guard let string = value as? String else { return nil }
|
|
277
|
+
let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
278
|
+
return trimmed.isEmpty ? nil : String(trimmed.prefix(200))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// The hosts images may come from (the plugin's `imageHosts`); none set means any https host.
|
|
282
|
+
private let imageHosts: [String] = ((Bundle.main.object(forInfoDictionaryKey: "GenesisRichPushImageHosts") as? [String]) ?? [])
|
|
283
|
+
.map { $0.lowercased() }
|
|
284
|
+
|
|
285
|
+
private func allowedHost(_ host: String) -> Bool {
|
|
286
|
+
if imageHosts.isEmpty { return true }
|
|
287
|
+
let h = host.lowercased()
|
|
288
|
+
return imageHosts.contains { h == $0 || h.hasSuffix("." + $0) }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Only https, with a host and no credentials, at most 1024 characters (the JS `isRichPushUrl`).
|
|
292
|
+
private func httpsURL(_ value: Any?) -> URL? {
|
|
293
|
+
guard let string = value as? String, string.count <= maxURLLength,
|
|
294
|
+
let components = URLComponents(string: string),
|
|
295
|
+
components.scheme?.lowercased() == "https",
|
|
296
|
+
let host = components.host, !host.isEmpty, allowedHost(host),
|
|
297
|
+
components.user == nil, components.password == nil
|
|
298
|
+
else { return nil }
|
|
299
|
+
return components.url
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func imageExtension(_ mime: String) -> String? {
|
|
303
|
+
switch mime {
|
|
304
|
+
case "image/jpeg", "image/jpg": return "jpg"
|
|
305
|
+
case "image/png": return "png"
|
|
306
|
+
case "image/gif": return "gif"
|
|
307
|
+
default: return nil
|
|
308
|
+
}
|
|
309
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marktiderman/genesis-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
|
-
"description": "The native app shell for Genesis Expo apps: one GenesisNativeApp root that runs boot steps behind the splash, catches errors into a fatal screen with retry, persists the query cache, gates on the Supabase session and normalizes deep links. An optional updates block (/updates) adds force upgrade, maintenance, an update banner and what's new. Every part is a slot and is also exported on its own. Part of the Genesis framework.",
|
|
7
|
+
"description": "The native app shell for Genesis Expo apps: one GenesisNativeApp root that runs boot steps behind the splash, catches errors into a fatal screen with retry, persists the query cache, gates on the Supabase session and normalizes deep links. An optional updates block (/updates) adds force upgrade, maintenance, an update banner and what's new; an optional push block (/push) adds push registration, tap routing, the badge and a notification settings screen with quiet hours and private previews; the observability block (/observability) reports errors, breadcrumbs, typed analytics events and timings through vendor-neutral sinks (Sentry, EAS Observe, console) with PII scrubbed on the device; the devtools overlay (/devtools) is a debug sheet for development and preview builds. Blocks (defineBlock declarations, through `blocks`) add boot steps, roots, slot fills and Settings rows. Every part is a slot and is also exported on its own. Part of the Genesis framework.",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/marktiderman/genesis.git",
|
|
@@ -18,48 +18,103 @@
|
|
|
18
18
|
"main": "./dist/index.js",
|
|
19
19
|
"module": "./dist/index.js",
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
|
+
"source": "./src/index.ts",
|
|
22
|
+
"react-native": "./src/index.ts",
|
|
21
23
|
"exports": {
|
|
22
24
|
"./package.json": "./package.json",
|
|
23
25
|
".": {
|
|
24
26
|
"types": "./dist/index.d.ts",
|
|
27
|
+
"react-native": "./src/index.ts",
|
|
25
28
|
"import": "./dist/index.js"
|
|
26
29
|
},
|
|
27
30
|
"./persist": {
|
|
28
31
|
"types": "./dist/persist.d.ts",
|
|
32
|
+
"react-native": "./src/persist.ts",
|
|
29
33
|
"import": "./dist/persist.js"
|
|
30
34
|
},
|
|
31
35
|
"./updates": {
|
|
32
36
|
"types": "./dist/updates.d.ts",
|
|
37
|
+
"react-native": "./src/updates/index.ts",
|
|
33
38
|
"import": "./dist/updates.js"
|
|
39
|
+
},
|
|
40
|
+
"./push": {
|
|
41
|
+
"types": "./dist/push.d.ts",
|
|
42
|
+
"react-native": "./src/push/index.ts",
|
|
43
|
+
"import": "./dist/push.js"
|
|
44
|
+
},
|
|
45
|
+
"./push-payload": {
|
|
46
|
+
"types": "./dist/push-payload.d.ts",
|
|
47
|
+
"import": "./dist/push-payload.js",
|
|
48
|
+
"default": "./dist/push-payload.js"
|
|
49
|
+
},
|
|
50
|
+
"./observability": {
|
|
51
|
+
"types": "./dist/observability.d.ts",
|
|
52
|
+
"react-native": "./src/observability/index.ts",
|
|
53
|
+
"import": "./dist/observability.js"
|
|
54
|
+
},
|
|
55
|
+
"./devtools": {
|
|
56
|
+
"types": "./dist/devtools.d.ts",
|
|
57
|
+
"react-native": "./src/devtools/index.ts",
|
|
58
|
+
"import": "./dist/devtools.js"
|
|
59
|
+
},
|
|
60
|
+
"./dev-warnings": {
|
|
61
|
+
"types": "./dist/dev-warnings.d.ts",
|
|
62
|
+
"react-native": "./src/dev-warnings.ts",
|
|
63
|
+
"import": "./dist/dev-warnings.js"
|
|
64
|
+
},
|
|
65
|
+
"./rich-push-plugin": {
|
|
66
|
+
"types": "./dist/rich-push-plugin.d.ts",
|
|
67
|
+
"import": "./dist/rich-push-plugin.js",
|
|
68
|
+
"default": "./dist/rich-push-plugin.js"
|
|
34
69
|
}
|
|
35
70
|
},
|
|
36
71
|
"files": [
|
|
37
72
|
"dist",
|
|
73
|
+
"src",
|
|
74
|
+
"ios",
|
|
75
|
+
"!src/**/__tests__",
|
|
38
76
|
"README.md"
|
|
39
77
|
],
|
|
40
78
|
"dependencies": {
|
|
41
79
|
"@marktiderman/genesis-core": "^1.7.0",
|
|
42
|
-
"@marktiderman/genesis-design-system": "^1.
|
|
43
|
-
"@marktiderman/genesis-
|
|
80
|
+
"@marktiderman/genesis-design-system": "^1.5.0",
|
|
81
|
+
"@marktiderman/genesis-features": "^0.3.0",
|
|
82
|
+
"@marktiderman/genesis-ui-native": "^2.4.0"
|
|
44
83
|
},
|
|
45
84
|
"peerDependencies": {
|
|
46
85
|
"@tanstack/query-async-storage-persister": "^5.0.0",
|
|
47
86
|
"@tanstack/query-persist-client-core": "^5.0.0",
|
|
48
87
|
"@tanstack/react-query": "^5.0.0",
|
|
88
|
+
"expo": ">=54",
|
|
49
89
|
"react": "^19.0.0",
|
|
50
90
|
"react-native": ">=0.76.0",
|
|
51
91
|
"react-native-safe-area-context": ">=4.0.0"
|
|
52
92
|
},
|
|
53
93
|
"peerDependenciesMeta": {
|
|
94
|
+
"@sentry/react-native": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
54
97
|
"@tanstack/query-async-storage-persister": {
|
|
55
98
|
"optional": true
|
|
56
99
|
},
|
|
57
100
|
"@tanstack/query-persist-client-core": {
|
|
58
101
|
"optional": true
|
|
59
102
|
},
|
|
103
|
+
"expo": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
60
106
|
"expo-application": {
|
|
61
107
|
"optional": true
|
|
62
108
|
},
|
|
109
|
+
"expo-notifications": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"expo-observe": {
|
|
113
|
+
"optional": true
|
|
114
|
+
},
|
|
115
|
+
"expo-sensors": {
|
|
116
|
+
"optional": true
|
|
117
|
+
},
|
|
63
118
|
"expo-updates": {
|
|
64
119
|
"optional": true
|
|
65
120
|
}
|
|
@@ -71,13 +126,191 @@
|
|
|
71
126
|
"@tanstack/react-query": "^5.75.0",
|
|
72
127
|
"@testing-library/react": "^16.3.2",
|
|
73
128
|
"@types/react": "^19.2.14",
|
|
129
|
+
"expo": "~58.0.19",
|
|
74
130
|
"happy-dom": "^20.9.0",
|
|
75
131
|
"react": "^19.2.5",
|
|
76
132
|
"react-dom": "^19.2.5",
|
|
77
133
|
"tsup": "^8.3.0",
|
|
78
|
-
"typescript": "
|
|
134
|
+
"typescript": "~6.0.3",
|
|
79
135
|
"vitest": "^4.1.12"
|
|
80
136
|
},
|
|
137
|
+
"genesis": {
|
|
138
|
+
"blocks": {
|
|
139
|
+
"push": {
|
|
140
|
+
"feature": {
|
|
141
|
+
"name": "Push and notification preferences",
|
|
142
|
+
"summary": "Registers this device's push token for whoever signs in, routes taps, and edits per-category preferences, quiet hours and previews.",
|
|
143
|
+
"keywords": [
|
|
144
|
+
"notifications",
|
|
145
|
+
"push token",
|
|
146
|
+
"quiet hours"
|
|
147
|
+
],
|
|
148
|
+
"audience": [
|
|
149
|
+
"person"
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
"platforms": [
|
|
153
|
+
"native"
|
|
154
|
+
],
|
|
155
|
+
"packages": [
|
|
156
|
+
"@marktiderman/genesis-native",
|
|
157
|
+
"@marktiderman/genesis-supabase"
|
|
158
|
+
],
|
|
159
|
+
"migrations": [
|
|
160
|
+
{
|
|
161
|
+
"package": "@marktiderman/genesis-supabase",
|
|
162
|
+
"file": "migrations/0005_genesis_push.sql",
|
|
163
|
+
"test": "migrations/tests/0005_genesis_push.test.sql"
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
"functions": [
|
|
167
|
+
{
|
|
168
|
+
"package": "@marktiderman/genesis-supabase",
|
|
169
|
+
"dir": "functions/genesis-push-send",
|
|
170
|
+
"verifyJwt": false,
|
|
171
|
+
"secrets": [
|
|
172
|
+
"GENESIS_PUSH_SECRET",
|
|
173
|
+
"EXPO_ACCESS_TOKEN"
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
"routes": [
|
|
178
|
+
{
|
|
179
|
+
"path": "/settings/notifications",
|
|
180
|
+
"description": "Notification settings: categories, quiet hours, previews",
|
|
181
|
+
"signedIn": true
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
"steps": [
|
|
185
|
+
"npx expo install expo-notifications, and add \"expo-notifications\" to the plugins in app.config.",
|
|
186
|
+
"Create the block with createPush (@marktiderman/genesis-native/push) and pass it to <GenesisNativeApp blocks>; render NotificationSettings on the /settings/notifications screen (genesis-native README, Push).",
|
|
187
|
+
"A push token needs an EAS project id (eas init)."
|
|
188
|
+
],
|
|
189
|
+
"privacy": {
|
|
190
|
+
"collects": [
|
|
191
|
+
{
|
|
192
|
+
"type": "device ID",
|
|
193
|
+
"purposes": [
|
|
194
|
+
"app functionality"
|
|
195
|
+
],
|
|
196
|
+
"linked": true,
|
|
197
|
+
"tracking": false
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
"app-updates": {
|
|
203
|
+
"feature": {
|
|
204
|
+
"name": "App updates",
|
|
205
|
+
"summary": "Force upgrade, a maintenance screen, an update banner and a what's new sheet, from the app_release_policy row.",
|
|
206
|
+
"keywords": [
|
|
207
|
+
"force upgrade",
|
|
208
|
+
"maintenance",
|
|
209
|
+
"ota"
|
|
210
|
+
],
|
|
211
|
+
"audience": [
|
|
212
|
+
"person"
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
"platforms": [
|
|
216
|
+
"native"
|
|
217
|
+
],
|
|
218
|
+
"packages": [
|
|
219
|
+
"@marktiderman/genesis-native",
|
|
220
|
+
"@marktiderman/genesis-supabase"
|
|
221
|
+
],
|
|
222
|
+
"migrations": [
|
|
223
|
+
{
|
|
224
|
+
"package": "@marktiderman/genesis-supabase",
|
|
225
|
+
"file": "migrations/0002_genesis_app_release_policy.sql",
|
|
226
|
+
"test": "migrations/tests/0002_genesis_app_release_policy.test.sql"
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
"steps": [
|
|
230
|
+
"npx expo install expo-updates expo-application.",
|
|
231
|
+
"Create the block with createUpdates (@marktiderman/genesis-native/updates) and pass it to <GenesisNativeApp updates> (genesis-native README, Updates)."
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
"observability": {
|
|
235
|
+
"feature": {
|
|
236
|
+
"name": "Observability",
|
|
237
|
+
"summary": "Crash reports, breadcrumbs, timings and typed analytics events, scrubbed on the device, through vendor-neutral sinks (Sentry included).",
|
|
238
|
+
"keywords": [
|
|
239
|
+
"crash reporting",
|
|
240
|
+
"sentry",
|
|
241
|
+
"analytics",
|
|
242
|
+
"breadcrumbs"
|
|
243
|
+
],
|
|
244
|
+
"audience": [
|
|
245
|
+
"developer"
|
|
246
|
+
]
|
|
247
|
+
},
|
|
248
|
+
"platforms": [
|
|
249
|
+
"native"
|
|
250
|
+
],
|
|
251
|
+
"steps": [
|
|
252
|
+
"npx expo install @sentry/react-native if you report to Sentry, and set the DSN in your environment (genesis-native README, Observability).",
|
|
253
|
+
"Create the block with createObservability (@marktiderman/genesis-native/observability) at module scope, pass it to <GenesisNativeApp blocks>, and export observability.wrap(RootLayout)."
|
|
254
|
+
],
|
|
255
|
+
"privacy": {
|
|
256
|
+
"collects": [
|
|
257
|
+
{
|
|
258
|
+
"type": "crash data",
|
|
259
|
+
"purposes": [
|
|
260
|
+
"app functionality"
|
|
261
|
+
],
|
|
262
|
+
"linked": true,
|
|
263
|
+
"tracking": false
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"type": "performance data",
|
|
267
|
+
"purposes": [
|
|
268
|
+
"app functionality"
|
|
269
|
+
],
|
|
270
|
+
"linked": true,
|
|
271
|
+
"tracking": false
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"type": "other diagnostic data",
|
|
275
|
+
"purposes": [
|
|
276
|
+
"app functionality"
|
|
277
|
+
],
|
|
278
|
+
"linked": true,
|
|
279
|
+
"tracking": false
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"type": "user id",
|
|
283
|
+
"purposes": [
|
|
284
|
+
"app functionality"
|
|
285
|
+
],
|
|
286
|
+
"linked": true,
|
|
287
|
+
"tracking": false
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"devtools-overlay": {
|
|
293
|
+
"feature": {
|
|
294
|
+
"name": "Devtools overlay",
|
|
295
|
+
"summary": "An on-device overlay for development, preview and internal builds: what the app knows about itself, recent activity, a crash test.",
|
|
296
|
+
"keywords": [
|
|
297
|
+
"debug menu",
|
|
298
|
+
"devtools",
|
|
299
|
+
"shake to debug"
|
|
300
|
+
],
|
|
301
|
+
"audience": [
|
|
302
|
+
"developer"
|
|
303
|
+
]
|
|
304
|
+
},
|
|
305
|
+
"platforms": [
|
|
306
|
+
"native"
|
|
307
|
+
],
|
|
308
|
+
"steps": [
|
|
309
|
+
"Create the block with createDevtools (@marktiderman/genesis-native/devtools), enabled only outside production builds, and pass it to <GenesisNativeApp blocks> (genesis-native README)."
|
|
310
|
+
]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
81
314
|
"scripts": {
|
|
82
315
|
"build": "tsup && node ../../scripts/check-exports-resolve.mjs",
|
|
83
316
|
"clean": "rm -rf dist",
|