@lynxship/expo 0.1.6 → 0.1.8

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/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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ComponentType, type ReactNode } from "react";
1
+ import { type ComponentType } from "react";
2
2
  import type { ViewProps } from "react-native";
3
3
  import { type LynxShipExpoConfig } from "./config.js";
4
4
  export type { LynxShipExpoConfig } from "./config.js";
@@ -8,14 +8,84 @@ export interface LynxViewProps extends ViewProps {
8
8
  bundle?: string;
9
9
  /** Optional initial data passed to Lynx when the view is rendered. */
10
10
  initialData?: string;
11
+ /** Global props exposed as `lynx.__globalProps` in the loaded Lynx page. */
12
+ globalProps?: Readonly<Record<string, unknown>>;
13
+ /** Injects the standard OS, size, safe-area and lifecycle host props. */
14
+ autoGlobalProps?: boolean;
11
15
  /** Requests a fresh render after an OTA candidate has been activated. */
12
16
  reloadOnUpdate?: boolean;
17
+ /** Emitted after Lynx reports that the first screen layout completed. */
18
+ onReady?: (event: LynxViewReadyEvent) => void;
19
+ /** Emitted when Lynx begins loading a bundle. */
20
+ onLoadStart?: (event: LynxViewLoadStartEvent) => void;
21
+ /** Emitted when the native provider starts fetching bundle bytes. */
22
+ onResourceFetchStart?: (event: LynxViewResourceFetchStartEvent) => void;
23
+ /** Emitted when the first rendered screen is available. */
24
+ onLoadSuccess?: (event: LynxViewReadyEvent) => void;
25
+ /** Emitted when the native host or bundle provider reports an error. */
26
+ onError?: (event: LynxViewErrorEvent) => void;
27
+ /** Emitted after a verified OTA candidate has been activated. */
28
+ onUpdate?: (event: LynxViewUpdateEvent) => void;
29
+ /** Emitted when the native Lynx view becomes visible. */
30
+ onShow?: () => void;
31
+ /** Emitted when the native Lynx view leaves the window. */
32
+ onHide?: () => void;
33
+ }
34
+ export interface LynxViewLoadStartEvent {
35
+ readonly nativeEvent: {
36
+ readonly bundle: string;
37
+ };
38
+ }
39
+ export interface LynxViewResourceFetchStartEvent {
40
+ readonly nativeEvent: {
41
+ readonly bundle: string;
42
+ };
43
+ }
44
+ export interface LynxViewReadyEvent {
45
+ readonly nativeEvent: {
46
+ readonly bundle: string;
47
+ readonly sequence: number;
48
+ };
49
+ }
50
+ export interface LynxViewErrorEvent {
51
+ readonly nativeEvent: {
52
+ readonly message: string;
53
+ readonly recoverable?: boolean;
54
+ };
55
+ }
56
+ export interface LynxViewUpdateEvent {
57
+ readonly nativeEvent: {
58
+ readonly sequence: number;
59
+ };
60
+ }
61
+ export interface LynxViewViewport {
62
+ readonly width: number;
63
+ readonly height: number;
64
+ }
65
+ export type LynxViewLoadState = "idle" | "loading" | "loaded" | "failed" | "released";
66
+ /** Imperative controls implemented by the native LynxView ref. */
67
+ export interface LynxViewRef {
68
+ getContainerId(): Promise<string>;
69
+ getLoadState(): Promise<LynxViewLoadState>;
70
+ isLoadSuccess(): Promise<boolean>;
71
+ reload(): Promise<void>;
72
+ /** Updates Lynx initData without remounting the native view. */
73
+ updateData(data: string, processorName?: string): Promise<void>;
74
+ updateGlobalProps(props: Readonly<Record<string, unknown>>): Promise<void>;
75
+ /** Merges a partial global-props patch without remounting the Lynx page. */
76
+ updateGlobalPropsByIncrement(props: Readonly<Record<string, unknown>>): Promise<void>;
77
+ sendGlobalEvent(eventName: string, params?: readonly unknown[]): Promise<void>;
78
+ show(): Promise<void>;
79
+ hide(): Promise<void>;
80
+ updateViewport(viewport: LynxViewViewport): Promise<void>;
13
81
  }
14
82
  /**
15
83
  * A LynxView that can be placed anywhere in an Expo/React Native view tree.
16
84
  * Native OTA configuration is supplied by the LynxShip config plugin.
17
85
  */
18
- export declare function LynxView(props: LynxViewProps): ReactNode;
86
+ export declare const LynxView: ComponentType<LynxViewProps & {
87
+ ref?: LynxViewRef | null | undefined;
88
+ }>;
19
89
  export declare const LynxShipView: ComponentType<LynxViewProps>;
20
90
  export declare function defineLynxShipExpoConfig(config: LynxShipExpoConfig): LynxShipExpoConfig;
21
91
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAID;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAOxD;AAED,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,aAAa,CAAY,CAAC;AAEnE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,kBAAkB,GACzB,kBAAkB,CAEpB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,OAAO,CAAC;AAEf,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AAErB,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChD,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,yEAAyE;IACzE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC9C,iDAAiD;IACjD,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACtD,qEAAqE;IACrE,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,IAAI,CAAC;IACxE,2DAA2D;IAC3D,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACpD,wEAAwE;IACxE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC9C,iEAAiE;IACjE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAChD,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,WAAW,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC3B,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC;CACH;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC3B,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,CAAC;AAEf,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC1B,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,YAAY,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC3C,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,gEAAgE;IAChE,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,4EAA4E;IAC5E,4BAA4B,CAC1B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,eAAe,CACb,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAMD;;;GAGG;AACH,eAAO,MAAM,QAAQ;;EAWpB,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,aAAa,CAAY,CAAC;AAEnE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,kBAAkB,GACzB,kBAAkB,CAEpB"}
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LynxShipView = exports.validateLynxShipExpoConfig = void 0;
4
- exports.LynxView = LynxView;
3
+ exports.LynxShipView = exports.LynxView = exports.validateLynxShipExpoConfig = void 0;
5
4
  exports.defineLynxShipExpoConfig = defineLynxShipExpoConfig;
6
5
  const react_1 = require("react");
7
6
  const expo_modules_core_1 = require("expo-modules-core");
@@ -13,15 +12,16 @@ const NativeLynxView = (0, expo_modules_core_1.requireNativeViewManager)("LynxSh
13
12
  * A LynxView that can be placed anywhere in an Expo/React Native view tree.
14
13
  * Native OTA configuration is supplied by the LynxShip config plugin.
15
14
  */
16
- function LynxView(props) {
17
- return (0, react_1.createElement)(NativeLynxView, {
18
- bundle: props.bundle ?? "main.lynx.bundle",
19
- initialData: props.initialData ?? "",
20
- reloadOnUpdate: props.reloadOnUpdate ?? true,
21
- ...props,
22
- });
23
- }
24
- exports.LynxShipView = LynxView;
15
+ exports.LynxView = (0, react_1.forwardRef)((props, ref) => (0, react_1.createElement)(NativeLynxView, {
16
+ bundle: props.bundle ?? "main.lynx.bundle",
17
+ initialData: props.initialData ?? "",
18
+ globalProps: props.globalProps ?? {},
19
+ autoGlobalProps: props.autoGlobalProps ?? true,
20
+ reloadOnUpdate: props.reloadOnUpdate ?? true,
21
+ ...props,
22
+ ref,
23
+ }));
24
+ exports.LynxShipView = exports.LynxView;
25
25
  function defineLynxShipExpoConfig(config) {
26
26
  return (0, config_js_1.validateLynxShipExpoConfig)(config);
27
27
  }
@@ -4,12 +4,18 @@ public class LynxShipExpoModule: Module {
4
4
  public func definition() -> ModuleDefinition {
5
5
  Name("LynxShip")
6
6
  View(LynxShipExpoView.self) {
7
- Events("onReady", "onError", "onUpdate")
7
+ Events("onReady", "onLoadStart", "onResourceFetchStart", "onLoadSuccess", "onError", "onUpdate", "onShow", "onHide")
8
8
  Prop("bundle") { (view: LynxShipExpoView, value: String?) in
9
- view.bundleName = value ?? "main.lynx.bundle"
9
+ view.setBundleName(value ?? "main.lynx.bundle")
10
10
  }
11
11
  Prop("initialData") { (view: LynxShipExpoView, value: String?) in
12
- view.initialData = value ?? ""
12
+ view.setInitialData(value ?? "")
13
+ }
14
+ Prop("globalProps") { (view: LynxShipExpoView, value: [String: Any]?) in
15
+ view.updateGlobalProps(value ?? [:])
16
+ }
17
+ Prop("autoGlobalProps") { (view: LynxShipExpoView, value: Bool?) in
18
+ view.autoGlobalProps = value ?? true
13
19
  }
14
20
  Prop("reloadOnUpdate") { (view: LynxShipExpoView, value: Bool?) in
15
21
  view.reloadOnUpdate = value ?? true
@@ -17,6 +23,36 @@ public class LynxShipExpoModule: Module {
17
23
  AsyncFunction("reload") { (view: LynxShipExpoView) in
18
24
  try view.reload()
19
25
  }
26
+ AsyncFunction("getContainerId") { (view: LynxShipExpoView) in
27
+ view.getContainerId()
28
+ }
29
+ AsyncFunction("getLoadState") { (view: LynxShipExpoView) in
30
+ view.getLoadState()
31
+ }
32
+ AsyncFunction("isLoadSuccess") { (view: LynxShipExpoView) in
33
+ view.isLoadSuccess()
34
+ }
35
+ AsyncFunction("updateData") { (view: LynxShipExpoView, data: String, processorName: String?) in
36
+ try view.updateData(data, processorName: processorName)
37
+ }
38
+ AsyncFunction("updateGlobalProps") { (view: LynxShipExpoView, props: [String: Any]) in
39
+ view.updateGlobalProps(props)
40
+ }
41
+ AsyncFunction("updateGlobalPropsByIncrement") { (view: LynxShipExpoView, props: [String: Any]) in
42
+ view.updateGlobalPropsByIncrement(props)
43
+ }
44
+ AsyncFunction("sendGlobalEvent") { (view: LynxShipExpoView, eventName: String, params: [Any]) in
45
+ try view.sendGlobalEvent(eventName, params: params)
46
+ }
47
+ AsyncFunction("show") { (view: LynxShipExpoView) in
48
+ view.show()
49
+ }
50
+ AsyncFunction("hide") { (view: LynxShipExpoView) in
51
+ view.hide()
52
+ }
53
+ AsyncFunction("updateViewport") { (view: LynxShipExpoView, viewport: [String: Double]) in
54
+ try view.updateViewport(viewport)
55
+ }
20
56
  }
21
57
  }
22
58
  }
@@ -6,38 +6,79 @@ import UIKit
6
6
 
7
7
  private final class LynxShipTemplateProvider: NSObject, LynxTemplateProvider {
8
8
  private let load: (String) throws -> Data
9
- private let onLoaded: () -> Void
9
+ private let onStart: (String) -> Void
10
10
 
11
- init(load: @escaping (String) throws -> Data, onLoaded: @escaping () -> Void) {
11
+ init(load: @escaping (String) throws -> Data, onStart: @escaping (String) -> Void) {
12
12
  self.load = load
13
- self.onLoaded = onLoaded
13
+ self.onStart = onStart
14
14
  }
15
15
 
16
16
  func loadTemplate(withUrl url: String!, onComplete callback: LynxTemplateLoadBlock!) {
17
+ onStart(url ?? "")
17
18
  do {
18
19
  callback(load(url), nil)
19
- onLoaded()
20
20
  } catch {
21
21
  callback(nil, error)
22
22
  }
23
23
  }
24
24
  }
25
25
 
26
+ private final class LynxShipLifecycleClient: NSObject, LynxViewLifecycle {
27
+ private let onStart: () -> Void
28
+ private let onFirstScreen: () -> Void
29
+
30
+ init(onStart: @escaping () -> Void, onFirstScreen: @escaping () -> Void) {
31
+ self.onStart = onStart
32
+ self.onFirstScreen = onFirstScreen
33
+ }
34
+
35
+ func lynxViewDidStartLoading(_ view: LynxView) {
36
+ onStart()
37
+ }
38
+
39
+ func lynxViewDidFirstScreen(_ view: LynxView) {
40
+ onFirstScreen()
41
+ }
42
+ }
43
+
26
44
  public final class LynxShipExpoView: ExpoView {
27
45
  let onReady = EventDispatcher()
46
+ let onLoadStart = EventDispatcher()
47
+ let onResourceFetchStart = EventDispatcher()
48
+ let onLoadSuccess = EventDispatcher()
28
49
  let onError = EventDispatcher()
29
50
  let onUpdate = EventDispatcher()
51
+ let onShow = EventDispatcher()
52
+ let onHide = EventDispatcher()
30
53
 
31
54
  private lazy var templateProvider = LynxShipTemplateProvider(
32
55
  load: { [weak self] path in
33
56
  guard let self else { throw NSError(domain: "LynxShip", code: 1, userInfo: [NSLocalizedDescriptionKey: "Lynx view was released"]) }
34
57
  return try self.otaClient?.openActiveAsset(path) ?? self.readEmbeddedAsset(path)
35
58
  },
36
- onLoaded: { [weak self] in
59
+ onStart: { [weak self] bundle in
60
+ DispatchQueue.main.async { self?.onResourceFetchStart(["bundle": bundle]) }
61
+ }
62
+ )
63
+
64
+ private lazy var lifecycleClient = LynxShipLifecycleClient(
65
+ onStart: { [weak self] in
37
66
  guard let self else { return }
38
- try? self.otaClient?.markLaunchSuccess()
67
+ self.loadState = "loading"
68
+ self.onLoadStart(["bundle": self.bundleName])
69
+ },
70
+ onFirstScreen: { [weak self] in
71
+ guard let self else { return }
72
+ self.loadState = "loaded"
73
+ do {
74
+ try self.otaClient?.markLaunchSuccess()
75
+ } catch {
76
+ self.onError(["message": error.localizedDescription, "recoverable": true])
77
+ }
39
78
  DispatchQueue.main.async {
40
- self.onReady(["bundle": self.bundleName, "sequence": self.otaClient?.activeSequence ?? 0])
79
+ let event: [String: Any] = ["bundle": self.bundleName, "sequence": self.otaClient?.activeSequence ?? 0]
80
+ self.onLoadSuccess(event)
81
+ self.onReady(event)
41
82
  }
42
83
  }
43
84
  )
@@ -51,15 +92,26 @@ public final class LynxShipExpoView: ExpoView {
51
92
 
52
93
  private var otaClient: LynxShipOtaClient?
53
94
  private var hasRendered = false
95
+ private var loadState = "idle"
54
96
  var bundleName = "main.lynx.bundle"
55
97
  var initialData = ""
98
+ private var globalProps: [String: Any] = [:]
56
99
  var reloadOnUpdate = true
100
+ var autoGlobalProps = true {
101
+ didSet {
102
+ if hasRendered { pushGlobalProps() }
103
+ }
104
+ }
105
+ private let containerID = UUID().uuidString
106
+ private let containerInitTime = ISO8601DateFormatter().string(from: Date())
107
+ private var appInBackground = false
57
108
 
58
109
  public required init(appContext: AppContext? = nil) {
59
110
  super.init(appContext: appContext)
60
111
  LynxEnv.sharedInstance()
61
112
  otaClient = makeOtaClient()
62
113
  addSubview(lynxView)
114
+ lynxView.addLifecycleClient(lifecycleClient)
63
115
  }
64
116
 
65
117
  public override func layoutSubviews() {
@@ -67,16 +119,30 @@ public final class LynxShipExpoView: ExpoView {
67
119
  lynxView.frame = bounds
68
120
  lynxView.preferredLayoutWidth = bounds.width
69
121
  lynxView.preferredLayoutHeight = bounds.height
122
+ lynxView.updateViewport(withPreferredLayoutWidth: bounds.width,
123
+ preferredLayoutHeight: bounds.height,
124
+ needLayout: true)
125
+ if hasRendered && autoGlobalProps { pushGlobalProps() }
70
126
  }
71
127
 
72
128
  public override func didMoveToWindow() {
73
129
  super.didMoveToWindow()
130
+ appInBackground = window == nil
131
+ if window != nil {
132
+ lynxView.onEnterForeground()
133
+ onShow([:])
134
+ } else {
135
+ lynxView.onEnterBackground()
136
+ onHide([:])
137
+ }
138
+ if hasRendered && autoGlobalProps { pushGlobalProps() }
74
139
  guard window != nil, !hasRendered else { return }
75
140
  do {
76
141
  try otaClient?.beginLaunch()
77
142
  render()
78
143
  checkForUpdate()
79
144
  } catch {
145
+ loadState = "failed"
80
146
  onError(["message": error.localizedDescription])
81
147
  }
82
148
  }
@@ -85,11 +151,167 @@ public final class LynxShipExpoView: ExpoView {
85
151
  render()
86
152
  }
87
153
 
154
+ func getContainerId() -> String { containerID }
155
+
156
+ func getLoadState() -> String { loadState }
157
+
158
+ func isLoadSuccess() -> Bool { loadState == "loaded" }
159
+
160
+ func updateData(_ data: String, processorName: String? = nil) throws {
161
+ guard data.utf8.count <= 8 * 1024 * 1024 else {
162
+ throw NSError(domain: "LynxShip", code: 8, userInfo: [NSLocalizedDescriptionKey: "Lynx update data is larger than 8 MiB"])
163
+ }
164
+ guard hasRendered else {
165
+ throw NSError(domain: "LynxShip", code: 9, userInfo: [NSLocalizedDescriptionKey: "Lynx view has not loaded a bundle"])
166
+ }
167
+ if let processorName {
168
+ guard !processorName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
169
+ processorName.count <= 256,
170
+ processorName.rangeOfCharacter(from: .controlCharacters) == nil else {
171
+ throw NSError(domain: "LynxShip", code: 10, userInfo: [NSLocalizedDescriptionKey: "Lynx data processor name is invalid"])
172
+ }
173
+ let templateData = LynxTemplateData(json: data)
174
+ templateData.markState(processorName)
175
+ lynxView.updateData(withTemplateData: templateData)
176
+ } else {
177
+ lynxView.updateData(withString: data)
178
+ }
179
+ initialData = data
180
+ onUpdate(["bundle": bundleName, "reason": "data"])
181
+ }
182
+
183
+ func setBundleName(_ value: String) {
184
+ guard value != bundleName else { return }
185
+ bundleName = value
186
+ if hasRendered { render() }
187
+ }
188
+
189
+ func setInitialData(_ value: String) {
190
+ guard value != initialData else { return }
191
+ initialData = value
192
+ if hasRendered { render() }
193
+ }
194
+
195
+ func updateGlobalProps(_ props: [String: Any]) {
196
+ globalProps = props
197
+ pushGlobalProps()
198
+ }
199
+
200
+ func updateGlobalPropsByIncrement(_ props: [String: Any]) {
201
+ guard !props.isEmpty else { return }
202
+ globalProps.merge(props) { _, newValue in newValue }
203
+ lynxView.updateGlobalProps(withDictionary: props)
204
+ }
205
+
206
+ func sendGlobalEvent(_ eventName: String, params: [Any]) throws {
207
+ guard !eventName.isEmpty else {
208
+ throw NSError(domain: "LynxShip", code: 4, userInfo: [NSLocalizedDescriptionKey: "Lynx global event name cannot be empty"])
209
+ }
210
+ guard eventName.count <= 256 else {
211
+ throw NSError(domain: "LynxShip", code: 5, userInfo: [NSLocalizedDescriptionKey: "Lynx global event name is too long"])
212
+ }
213
+ guard params.count <= 256 else {
214
+ throw NSError(domain: "LynxShip", code: 6, userInfo: [NSLocalizedDescriptionKey: "Lynx global event payload is too large"])
215
+ }
216
+ lynxView.sendGlobalEvent(eventName, withParams: params)
217
+ }
218
+
219
+ func show() {
220
+ isHidden = false
221
+ }
222
+
223
+ func hide() {
224
+ isHidden = true
225
+ }
226
+
227
+ func updateViewport(_ viewport: [String: Double]) throws {
228
+ let width = viewport["width"] ?? 0
229
+ let height = viewport["height"] ?? 0
230
+ guard width.isFinite && height.isFinite && width >= 0 && height >= 0 else {
231
+ throw NSError(domain: "LynxShip", code: 7, userInfo: [NSLocalizedDescriptionKey: "Lynx viewport dimensions must be finite and non-negative"])
232
+ }
233
+ lynxView.updateViewport(withPreferredLayoutWidth: CGFloat(width),
234
+ preferredLayoutHeight: CGFloat(height),
235
+ needLayout: true)
236
+ }
237
+
88
238
  private func render() {
239
+ loadState = "loading"
240
+ if autoGlobalProps || !globalProps.isEmpty {
241
+ lynxView.updateGlobalProps(withDictionary: effectiveGlobalProps())
242
+ }
89
243
  lynxView.loadTemplate(fromURL: bundleName, initData: initialData)
90
244
  hasRendered = true
91
245
  }
92
246
 
247
+ private func pushGlobalProps() {
248
+ guard autoGlobalProps || !globalProps.isEmpty else { return }
249
+ lynxView.updateGlobalProps(withDictionary: effectiveGlobalProps())
250
+ }
251
+
252
+ private func effectiveGlobalProps() -> [String: Any] {
253
+ guard autoGlobalProps else { return globalProps }
254
+ let screen = window?.screen ?? UIScreen.main
255
+ let screenSize = screen.bounds.size
256
+ let insets = safeAreaInsets
257
+ let width = max(0, bounds.width)
258
+ let height = max(0, bounds.height)
259
+ let contentWidth = max(0, width - insets.left - insets.right)
260
+ let contentHeight = max(0, height - insets.top - insets.bottom)
261
+ let orientation: String
262
+ switch window?.windowScene?.interfaceOrientation {
263
+ case .landscapeLeft: orientation = "landscape-left"
264
+ case .landscapeRight: orientation = "landscape-right"
265
+ case .portraitUpsideDown: orientation = "portrait-upside-down"
266
+ case .portrait: orientation = "portrait"
267
+ default: orientation = width > height ? "landscape" : "portrait"
268
+ }
269
+ let theme: String
270
+ switch traitCollection.userInterfaceStyle {
271
+ case .dark: theme = "dark"
272
+ case .light: theme = "light"
273
+ default: theme = "system"
274
+ }
275
+ let locale = Locale.current.identifier
276
+ let language = Locale.current.languageCode ?? locale.split(separator: "_").first.map(String.init) ?? locale
277
+ let isTablet = UIDevice.current.userInterfaceIdiom == .pad
278
+ let isNotchScreen = UIDevice.current.userInterfaceIdiom == .phone && insets.top > 20
279
+ var props = globalProps
280
+ props["os"] = "ios"
281
+ props["osVersion"] = UIDevice.current.systemVersion
282
+ props["deviceModel"] = UIDevice.current.model
283
+ props["containerID"] = containerID
284
+ props["containerInitTime"] = containerInitTime
285
+ props["screenWidth"] = screenSize.width
286
+ props["screenHeight"] = screenSize.height
287
+ props["contentWidth"] = contentWidth
288
+ props["contentHeight"] = contentHeight
289
+ props["safeAreaInsets"] = [
290
+ "top": insets.top,
291
+ "right": insets.right,
292
+ "bottom": insets.bottom,
293
+ "left": insets.left,
294
+ ]
295
+ props["pixelRatio"] = screen.scale
296
+ props["accessibleMode"] = UIAccessibility.isVoiceOverRunning ? 1 : 0
297
+ props["isIPhoneX"] = isNotchScreen ? 1 : 0
298
+ props["isIPhoneXMax"] = isNotchScreen ? 1 : 0
299
+ props["isPad"] = isTablet ? 1 : 0
300
+ props["isNotchScreen"] = isNotchScreen
301
+ props["isLowPowerMode"] = ProcessInfo.processInfo.isLowPowerModeEnabled ? 1 : 0
302
+ props["orientation"] = orientation
303
+ props["screenOrientation"] = orientation
304
+ props["theme"] = theme
305
+ props["appLanguage"] = language
306
+ props["appLocale"] = locale
307
+ props["isAppBackground"] = appInBackground
308
+ props["queryItems"] = [String: String]()
309
+ props["topHeight"] = insets.top
310
+ props["bottomHeight"] = insets.bottom
311
+ props["safeAreaHeight"] = insets.top
312
+ return props
313
+ }
314
+
93
315
  private func checkForUpdate() {
94
316
  Task { [weak self] in
95
317
  guard let self else { return }
@@ -150,4 +372,9 @@ public final class LynxShipExpoView: ExpoView {
150
372
  return nil
151
373
  }
152
374
  }
375
+
376
+ deinit {
377
+ loadState = "released"
378
+ lynxView.destroy()
379
+ }
153
380
  }
package/package.json CHANGED
@@ -1,17 +1,23 @@
1
1
  {
2
2
  "name": "@lynxship/expo",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Expo native LynxView with LynxShip OTA delivery and cache support.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "files": [
9
9
  "dist",
10
- "android",
11
- "ios",
10
+ "android/build.gradle",
11
+ "android/consumer-rules.pro",
12
+ "android/src",
13
+ "ios/*.podspec",
14
+ "ios/*.h",
15
+ "ios/*.m",
16
+ "ios/*.swift",
12
17
  "src",
13
18
  "app.plugin.js",
14
19
  "app.plugin.cjs",
20
+ "asset-sync.cjs.d.cts",
15
21
  "asset-sync.cjs",
16
22
  "expo-module.config.json",
17
23
  "lynxship-expo.podspec",
@@ -42,6 +48,7 @@
42
48
  "peerDependencies": {
43
49
  "@expo/config-plugins": ">=9",
44
50
  "expo": ">=52",
51
+ "expo-modules-core": ">=52",
45
52
  "react": ">=18",
46
53
  "react-native": ">=0.76"
47
54
  },
@@ -49,8 +56,8 @@
49
56
  "expo-modules-core": "57.0.13"
50
57
  },
51
58
  "dependencies": {
52
- "@lynxship/sdk-ios": "0.1.0",
53
- "@lynxship/sdk-android": "0.1.0"
59
+ "@lynxship/sdk-android": "0.1.0",
60
+ "@lynxship/sdk-ios": "0.1.0"
54
61
  },
55
62
  "scripts": {
56
63
  "build": "tsc -b tsconfig.json",
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" &&