@lynxship/expo 0.1.5 → 0.1.7

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 CHANGED
@@ -19,9 +19,10 @@ npx pod-install
19
19
  ```
20
20
 
21
21
  `@lynxship/expo` uses the `expo-modules-core` implementation supplied by the
22
- installed Expo SDK. The package keeps it as a development dependency for its
23
- own native-module build, so installing `@lynxship/expo` does not require a
24
- second manual native dependency in the application.
22
+ installed Expo SDK. It is declared as a peer dependency so Expo Doctor can
23
+ validate the application explicitly; `npx expo install @lynxship/expo` should
24
+ select the SDK-compatible version. The workspace keeps a matching development
25
+ dependency for its own native-module build.
25
26
 
26
27
  When the project uses a static `app.json` or `app.config.json`, the Expo CLI
27
28
  automatically adds `@lynxship/expo` to `expo.plugins` during `npx expo install`.
@@ -43,6 +44,12 @@ options are needed, add the config plugin manually:
43
44
  "projectId": "00000000-0000-4000-8000-000000000000",
44
45
  "channel": "production",
45
46
  "runtimeVersion": "lynx-runtime-2026-01",
47
+ "notifications": {
48
+ "enabled": true,
49
+ "enableBackgroundRemoteNotifications": true,
50
+ "communicationNotifications": true,
51
+ "android": { "defaultChannel": "default" }
52
+ },
46
53
  "publicKeys": {
47
54
  "release-key-1": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
48
55
  },
@@ -90,6 +97,32 @@ you intentionally operate a pinned native compatibility lane; do not put
90
97
  `latest` in a production lockfile without reviewing the resulting native
91
98
  build.
92
99
 
100
+ ## Optional push notifications
101
+
102
+ When an Expo app also uses `@lynxship/notifications/expo`, set
103
+ `notifications.enabled` to `true` in this plugin. LynxShip delegates the
104
+ native permission, FCM/APNs token acquisition and build-time configuration to
105
+ the official `expo-notifications` package, then the JavaScript adapter sends
106
+ the token to the authenticated backend. Install the SDK-selected native
107
+ package first:
108
+
109
+ ```bash
110
+ npx expo install expo-notifications expo-constants
111
+ ```
112
+
113
+ The app must call `LynxShipNotifications.register(...)` with its authenticated
114
+ user/project identity and HTTPS registration endpoint. This is intentionally
115
+ not guessed by the plugin: app identity and backend authorization are
116
+ application data. FCM/APNs credentials also remain in EAS/CI secrets.
117
+
118
+ For message or presence notifications with profile images, pass an HTTPS
119
+ `imageUrl` from the backend payload. Android can render the image directly
120
+ through FCM. iOS requires a separate Notification Service Extension target;
121
+ use the template published by `@lynxship/notifications` and add it to the Expo
122
+ iOS project before an EAS build. The extension is a separately signed native
123
+ target, so it cannot be created safely by the JavaScript `LynxView` component
124
+ at runtime.
125
+
93
126
  ## Use the view
94
127
 
95
128
  ```tsx
@@ -1,5 +1,6 @@
1
1
  package com.lynxship.expo
2
2
 
3
+ import android.app.Application
3
4
  import android.content.Context
4
5
  import android.content.SharedPreferences
5
6
  import android.os.Bundle
@@ -19,7 +20,7 @@ import com.lynx.tasm.provider.AbsTemplateProvider
19
20
  import com.lynx.tasm.service.LynxServiceCenter
20
21
  import com.lynxship.sdk.android.LynxShipOtaClient
21
22
  import expo.modules.kotlin.AppContext
22
- import expo.modules.kotlin.events.EventDispatcher
23
+ import expo.modules.kotlin.viewevent.EventDispatcher
23
24
  import expo.modules.kotlin.views.ExpoView
24
25
  import java.io.ByteArrayOutputStream
25
26
  import java.io.File
@@ -54,7 +55,9 @@ class LynxShipExpoView(context: Context, appContext: AppContext) : ExpoView(cont
54
55
 
55
56
  init {
56
57
  initializeLynx(context)
57
- lynxView = LynxViewBuilder().setTemplateProvider(templateProvider).build(applicationContext)
58
+ // The Lynx view must be created with the host/view context. Keep the
59
+ // application context only for process-wide services and storage.
60
+ lynxView = LynxViewBuilder().setTemplateProvider(templateProvider).build(context)
58
61
  lynxView.addLynxViewClient(object : LynxViewClient() {
59
62
  override fun onFirstScreen() {
60
63
  try {
@@ -62,7 +65,7 @@ class LynxShipExpoView(context: Context, appContext: AppContext) : ExpoView(cont
62
65
  } catch (error: IOException) {
63
66
  onError(mapOf("message" to (error.message ?: "Could not record Lynx launch"), "recoverable" to true))
64
67
  }
65
- onReady(mapOf("bundle" to bundleName, "sequence" to (otaClient?.activeSequence ?: 0)))
68
+ onReady(mapOf("bundle" to bundleName, "sequence" to (otaClient?.activeSequence() ?: 0)))
66
69
  }
67
70
  })
68
71
  addView(lynxView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
@@ -98,7 +101,7 @@ class LynxShipExpoView(context: Context, appContext: AppContext) : ExpoView(cont
98
101
  try {
99
102
  otaClient.activateCandidate()
100
103
  post {
101
- onUpdate(mapOf("sequence" to otaClient.activeSequence))
104
+ onUpdate(mapOf("sequence" to otaClient.activeSequence()))
102
105
  if (reloadOnUpdate) render()
103
106
  }
104
107
  } catch (error: Exception) {
@@ -168,6 +171,8 @@ class LynxShipExpoView(context: Context, appContext: AppContext) : ExpoView(cont
168
171
  synchronized(LynxShipExpoView::class.java) {
169
172
  if (!initialized) {
170
173
  val applicationContext = context.applicationContext
174
+ val application = applicationContext as? Application
175
+ ?: throw IllegalStateException("LynxShip requires an Android Application context")
171
176
  // Lynx's official Android integration requires these services
172
177
  // before a LynxView is created. Expo does not give this package
173
178
  // an application subclass, so initialize them once at the
@@ -184,7 +189,7 @@ class LynxShipExpoView(context: Context, appContext: AppContext) : ExpoView(cont
184
189
  LynxServiceCenter.inst().registerService(LynxImageService.getInstance())
185
190
  LynxServiceCenter.inst().registerService(LynxLogService)
186
191
  LynxServiceCenter.inst().registerService(LynxHttpService)
187
- LynxEnv.inst().init(applicationContext, null, null, null)
192
+ LynxEnv.inst().init(application, null, null, null)
188
193
  initialized = true
189
194
  }
190
195
  }
package/app.plugin.cjs CHANGED
@@ -7,6 +7,7 @@ const {
7
7
  withSettingsGradle,
8
8
  withXcodeProject,
9
9
  IOSConfig,
10
+ withPlugins,
10
11
  } = require("@expo/config-plugins");
11
12
  const path = require("node:path");
12
13
  const { syncLynxAssets } = require("./asset-sync.cjs");
@@ -78,6 +79,31 @@ function assertOptions(options) {
78
79
  typeof options.syncBundle !== "boolean"
79
80
  )
80
81
  throw new Error("@lynxship/expo syncBundle must be a boolean");
82
+ if (options.notifications !== undefined) {
83
+ if (
84
+ typeof options.notifications !== "object" ||
85
+ options.notifications === null ||
86
+ typeof options.notifications.enabled !== "boolean"
87
+ )
88
+ throw new Error(
89
+ "@lynxship/expo notifications must be an object with boolean enabled",
90
+ );
91
+ if (
92
+ options.notifications.enableBackgroundRemoteNotifications !== undefined &&
93
+ typeof options.notifications.enableBackgroundRemoteNotifications !==
94
+ "boolean"
95
+ )
96
+ throw new Error(
97
+ "@lynxship/expo enableBackgroundRemoteNotifications must be a boolean",
98
+ );
99
+ }
100
+ if (
101
+ options.notifications?.communicationNotifications !== undefined &&
102
+ typeof options.notifications.communicationNotifications !== "boolean"
103
+ )
104
+ throw new Error(
105
+ "@lynxship/expo communicationNotifications must be a boolean",
106
+ );
81
107
  if (
82
108
  options.embeddedBundle !== undefined &&
83
109
  (typeof options.embeddedBundle !== "string" ||
@@ -157,6 +183,14 @@ function withLynxShipInfoPlist(config, options) {
157
183
  publicKeys: options.publicKeys || {},
158
184
  maxReleaseBytes: options.maxReleaseBytes || 104857600,
159
185
  };
186
+ if (options.notifications?.communicationNotifications) {
187
+ const activityTypes = Array.isArray(value.modResults.NSUserActivityTypes)
188
+ ? value.modResults.NSUserActivityTypes
189
+ : [];
190
+ if (!activityTypes.includes("INSendMessageIntent"))
191
+ activityTypes.push("INSendMessageIntent");
192
+ value.modResults.NSUserActivityTypes = activityTypes;
193
+ }
160
194
  return value;
161
195
  });
162
196
  }
@@ -286,6 +320,28 @@ function withLynxShipIosAssets(config, options) {
286
320
  });
287
321
  }
288
322
 
323
+ function withLynxShipNotifications(config, options) {
324
+ const notifications = options.notifications;
325
+ if (!notifications?.enabled) return config;
326
+ try {
327
+ return withPlugins(config, [
328
+ [
329
+ "expo-notifications",
330
+ {
331
+ ...(notifications.android || {}),
332
+ enableBackgroundRemoteNotifications:
333
+ notifications.enableBackgroundRemoteNotifications || false,
334
+ },
335
+ ],
336
+ ]);
337
+ } catch (error) {
338
+ throw new Error(
339
+ "@lynxship/expo notifications requires expo-notifications. Install it with `npx expo install expo-notifications`.",
340
+ { cause: error },
341
+ );
342
+ }
343
+ }
344
+
289
345
  function withLynxShipExpo(config, props = {}) {
290
346
  const options = assertOptions({ ...getOptions(config), ...props });
291
347
  let result = withLynxShipAndroidManifest(config, options);
@@ -294,7 +350,8 @@ function withLynxShipExpo(config, props = {}) {
294
350
  result = withLynxShipAndroidSdk(result);
295
351
  result = withLynxShipAndroidAssets(result, options);
296
352
  result = withLynxShipIosAssets(result, options);
297
- return withLynxShipPodfile(result, options);
353
+ result = withLynxShipPodfile(result, options);
354
+ return withLynxShipNotifications(result, options);
298
355
  }
299
356
 
300
357
  module.exports = withLynxShipExpo;
@@ -0,0 +1,43 @@
1
+ declare const assetSync: {
2
+ createBundlePlan(
3
+ projectRoot: string,
4
+ options?: {
5
+ bundlePath?: string;
6
+ embeddedBundle?: string;
7
+ },
8
+ ): Promise<{
9
+ sourceBundle: string;
10
+ sourceDirectory: string;
11
+ bundlePath: string;
12
+ embeddedBundle: string;
13
+ files: Array<{
14
+ absolute: string;
15
+ relative: string;
16
+ destination: string;
17
+ }>;
18
+ }>;
19
+ syncBundleDirectory(options: {
20
+ projectRoot: string;
21
+ plan: Awaited<ReturnType<typeof assetSync.createBundlePlan>>;
22
+ destinationRoot: string;
23
+ manifestPath: string;
24
+ platform: "android" | "ios";
25
+ }): Promise<{
26
+ platform: "android" | "ios";
27
+ sourceBundle: string;
28
+ destinationRoot: string;
29
+ manifestPath: string;
30
+ files: Array<{ path: string; size: number; sha256: string }>;
31
+ }>;
32
+ syncLynxAssets(
33
+ projectRoot: string,
34
+ options?: {
35
+ platform?: "android" | "ios";
36
+ bundlePath?: string;
37
+ embeddedBundle?: string;
38
+ iosSourceRoot?: string;
39
+ },
40
+ ): Promise<unknown>;
41
+ };
42
+
43
+ export = assetSync;
package/dist/config.d.ts CHANGED
@@ -11,6 +11,17 @@ export interface LynxShipExpoConfig {
11
11
  syncBundle?: boolean;
12
12
  lynxVersion?: string;
13
13
  maxReleaseBytes?: number;
14
+ notifications?: {
15
+ enabled?: boolean;
16
+ enableBackgroundRemoteNotifications?: boolean;
17
+ /** Add the iOS messaging intent declaration for rich communication alerts. */
18
+ communicationNotifications?: boolean;
19
+ android?: {
20
+ icon?: string;
21
+ color?: string;
22
+ defaultChannel?: string;
23
+ };
24
+ };
14
25
  }
15
26
  export declare function validateLynxShipExpoConfig(value?: LynxShipExpoConfig): LynxShipExpoConfig;
16
27
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAKD,wBAAgB,0BAA0B,CACxC,KAAK,GAAE,kBAAuB,GAC7B,kBAAkB,CAgDpB"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,mCAAmC,CAAC,EAAE,OAAO,CAAC;QAC9C,8EAA8E;QAC9E,0BAA0B,CAAC,EAAE,OAAO,CAAC;QACrC,OAAO,CAAC,EAAE;YACR,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,KAAK,CAAC,EAAE,MAAM,CAAC;YACf,cAAc,CAAC,EAAE,MAAM,CAAC;SACzB,CAAC;KACH,CAAC;CACH;AAKD,wBAAgB,0BAA0B,CACxC,KAAK,GAAE,kBAAuB,GAC7B,kBAAkB,CAyEpB"}
package/dist/config.js CHANGED
@@ -25,6 +25,19 @@ function validateLynxShipExpoConfig(value = {}) {
25
25
  }
26
26
  if (value.syncBundle !== undefined && typeof value.syncBundle !== "boolean")
27
27
  throw new Error("LynxShip Expo syncBundle must be a boolean");
28
+ if (value.notifications !== undefined) {
29
+ if (typeof value.notifications !== "object" ||
30
+ value.notifications === null ||
31
+ typeof value.notifications.enabled !== "boolean")
32
+ throw new Error("LynxShip Expo notifications must be an object with boolean enabled");
33
+ if (value.notifications.enableBackgroundRemoteNotifications !== undefined &&
34
+ typeof value.notifications.enableBackgroundRemoteNotifications !==
35
+ "boolean")
36
+ throw new Error("LynxShip Expo enableBackgroundRemoteNotifications must be a boolean");
37
+ if (value.notifications.communicationNotifications !== undefined &&
38
+ typeof value.notifications.communicationNotifications !== "boolean")
39
+ throw new Error("LynxShip Expo communicationNotifications must be a boolean");
40
+ }
28
41
  if (value.lynxVersion !== undefined &&
29
42
  value.lynxVersion !== "auto" &&
30
43
  value.lynxVersion !== "latest" &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxship/expo",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Expo native LynxView with LynxShip OTA delivery and cache support.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -12,6 +12,7 @@
12
12
  "src",
13
13
  "app.plugin.js",
14
14
  "app.plugin.cjs",
15
+ "asset-sync.cjs.d.cts",
15
16
  "asset-sync.cjs",
16
17
  "expo-module.config.json",
17
18
  "lynxship-expo.podspec",
@@ -42,6 +43,7 @@
42
43
  "peerDependencies": {
43
44
  "@expo/config-plugins": ">=9",
44
45
  "expo": ">=52",
46
+ "expo-modules-core": ">=52",
45
47
  "react": ">=18",
46
48
  "react-native": ">=0.76"
47
49
  },
package/src/config.ts CHANGED
@@ -11,6 +11,17 @@ export interface LynxShipExpoConfig {
11
11
  syncBundle?: boolean;
12
12
  lynxVersion?: string;
13
13
  maxReleaseBytes?: number;
14
+ notifications?: {
15
+ enabled?: boolean;
16
+ enableBackgroundRemoteNotifications?: boolean;
17
+ /** Add the iOS messaging intent declaration for rich communication alerts. */
18
+ communicationNotifications?: boolean;
19
+ android?: {
20
+ icon?: string;
21
+ color?: string;
22
+ defaultChannel?: string;
23
+ };
24
+ };
14
25
  }
15
26
 
16
27
  /** Let the native package manager resolve the current Lynx SDK by default. */
@@ -48,6 +59,31 @@ export function validateLynxShipExpoConfig(
48
59
  }
49
60
  if (value.syncBundle !== undefined && typeof value.syncBundle !== "boolean")
50
61
  throw new Error("LynxShip Expo syncBundle must be a boolean");
62
+ if (value.notifications !== undefined) {
63
+ if (
64
+ typeof value.notifications !== "object" ||
65
+ value.notifications === null ||
66
+ typeof value.notifications.enabled !== "boolean"
67
+ )
68
+ throw new Error(
69
+ "LynxShip Expo notifications must be an object with boolean enabled",
70
+ );
71
+ if (
72
+ value.notifications.enableBackgroundRemoteNotifications !== undefined &&
73
+ typeof value.notifications.enableBackgroundRemoteNotifications !==
74
+ "boolean"
75
+ )
76
+ throw new Error(
77
+ "LynxShip Expo enableBackgroundRemoteNotifications must be a boolean",
78
+ );
79
+ if (
80
+ value.notifications.communicationNotifications !== undefined &&
81
+ typeof value.notifications.communicationNotifications !== "boolean"
82
+ )
83
+ throw new Error(
84
+ "LynxShip Expo communicationNotifications must be a boolean",
85
+ );
86
+ }
51
87
  if (
52
88
  value.lynxVersion !== undefined &&
53
89
  value.lynxVersion !== "auto" &&
@@ -1,5 +1,15 @@
1
1
  declare module "react" {
2
- export type ReactNode = unknown;
2
+ export interface ReactElement {
3
+ readonly type: unknown;
4
+ readonly props: unknown;
5
+ }
6
+ export type ReactNode =
7
+ | ReactElement
8
+ | string
9
+ | number
10
+ | boolean
11
+ | null
12
+ | undefined;
3
13
  export type ComponentType<Props = Record<string, unknown>> = (
4
14
  props: Props,
5
15
  ) => ReactNode;