@webority/mobile-core 0.4.1 → 0.5.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 CHANGED
@@ -32,6 +32,7 @@ import { shareFile } from '@webority/mobile-core/share';
32
32
  import { initPush, enablePush, usePush } from '@webority/mobile-core/push';
33
33
  import { definePushActionTask } from '@webority/mobile-core/push/task';
34
34
  import { compressImage } from '@webority/mobile-core/compressor';
35
+ import { initOtaUpdates, useOtaUpdates } from '@webority/mobile-core/updates';
35
36
  import { createApiBaseQuery, createAppApi, setupApiListeners } from '@webority/mobile-core/query';
36
37
  ```
37
38
 
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"DEFAULT_MIN_CHECK_INTERVAL_MS",{enumerable:!0,get:function(){return _updates.DEFAULT_MIN_CHECK_INTERVAL_MS}}),Object.defineProperty(exports,"applyOtaUpdate",{enumerable:!0,get:function(){return _updates.applyOtaUpdate}}),Object.defineProperty(exports,"checkForOtaUpdate",{enumerable:!0,get:function(){return _updates.checkForOtaUpdate}}),Object.defineProperty(exports,"initOtaUpdates",{enumerable:!0,get:function(){return _updates.initOtaUpdates}}),Object.defineProperty(exports,"normalizeAppIdentity",{enumerable:!0,get:function(){return _updates.normalizeAppIdentity}}),Object.defineProperty(exports,"setOtaUpdatesImplementation",{enumerable:!0,get:function(){return _updates.setOtaUpdatesImplementation}}),Object.defineProperty(exports,"stopOtaUpdates",{enumerable:!0,get:function(){return _updates.stopOtaUpdates}}),Object.defineProperty(exports,"useOtaUpdates",{enumerable:!0,get:function(){return _updates.useOtaUpdates}});var _updates=require("./updates.js");
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.useOtaUpdates=exports.stopOtaUpdates=exports.setOtaUpdatesImplementation=exports.normalizeAppIdentity=exports.initOtaUpdates=exports.checkForOtaUpdate=exports.applyOtaUpdate=exports.DEFAULT_MIN_CHECK_INTERVAL_MS=void 0;var _react=require("react"),_reactNative=require("react-native"),_index=require("../config/index.js"),_index2=require("../logger/index.js"),_index3=require("../storage/index.js");const PREFIX="[@webority/mobile-core] OTA updates:",REPORTED_KEY="@webority/ota/last-reported-update-id",ENV_IDENTITY_SUFFIXES=[".staging",".development"],DEFAULT_MIN_CHECK_INTERVAL_MS=exports.DEFAULT_MIN_CHECK_INTERVAL_MS=9e5,EVENT_TIMEOUT_MS=1e4;let resolved,override=null;const setOtaUpdatesImplementation=e=>{override=e};exports.setOtaUpdatesImplementation=setOtaUpdatesImplementation;const resolveUpdates=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{resolved=require("expo-updates")}catch{resolved=null}return resolved},INITIAL_STATE={status:"unavailable",runningUpdateId:null,error:null,lastCheckedAt:null};let state=INITIAL_STATE;const subscribers=new Set,setState=e=>{state={...state,...e};for(const e of subscribers)e()};let config=null,stateSubscription=null,appStateSubscription=null,checking=null,lastCheckStartedAt=0,lastReportedError=null;const messageOf=e=>e instanceof Error?e.message:String(e),normalizeAppIdentity=e=>{const t=String(e??"").trim().toLowerCase(),a=ENV_IDENTITY_SUFFIXES.find(e=>t.endsWith(e));return a?t.slice(0,-a.length):t};exports.normalizeAppIdentity=normalizeAppIdentity;const ownIdentity=()=>config?.appIdentifier??(0,_index.getConfigValue)("appId",null)??null,identityMismatch=e=>{const t=e?.extra?.webority?.appIdentifier;if(!t)return null;const a=ownIdentity();return a&&normalizeAppIdentity(t)===normalizeAppIdentity(a)?null:`identity mismatch: the update is for '${t}' but this app is '${a??"unset"}'`},postEvent=async(e,t,a)=>{const r=config?.serverUrl;if(!r||!e)return;const s=new AbortController,n=setTimeout(()=>s.abort(),1e4);try{await fetch(`${r.replace(/\/+$/,"")}/ota/updates/${encodeURIComponent(e)}/events`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:t,platform:_reactNative.Platform.OS,runtimeVersion:resolveUpdates()?.runtimeVersion??null,message:a}),signal:s.signal})}catch(e){_index2.Logger.info(`${PREFIX} the ${t} event was not reported: ${messageOf(e)}`)}finally{clearTimeout(n)}},reportError=(e,t)=>{setState({status:"error",error:t});const a=`${e??""}|${t}`;a!==lastReportedError&&(lastReportedError=a,_index2.Logger.error(`${PREFIX} ${t}`),postEvent(e,"error",t))},reportRunningUpdate=async e=>{if(e.isEmbeddedLaunch||!e.updateId)return;const t=identityMismatch(e.manifest);t?reportError(e.updateId,t):await _index3.Storage.getItem(REPORTED_KEY)!==e.updateId&&(await postEvent(e.updateId,"success"),await _index3.Storage.setItem(REPORTED_KEY,e.updateId),_index2.Logger.info(`${PREFIX} running update ${e.updateId}`))},onStateChange=e=>{const t=e.downloadError??e.checkError;if(t){const a=e.latestManifest?.id??resolveUpdates()?.updateId;return void reportError(a,t.message)}e.isChecking?setState({status:"checking"}):e.isDownloading?setState({status:"downloading"}):e.isUpdatePending&&setState({status:"ready",error:null})},checkForOtaUpdate=()=>{const e=resolveUpdates();if(!config||!e?.isEnabled)return Promise.resolve("unavailable");if(checking)return checking;lastCheckStartedAt=Date.now();return checking=(async()=>{try{setState({status:"checking"});const t=await e.checkForUpdateAsync();if(t.isAvailable){const e=identityMismatch(t.manifest);if(e)return reportError(t.manifest?.id,e),"error"}return t.isAvailable||t.isRollBackToEmbedded?(setState({status:"downloading"}),await e.fetchUpdateAsync(),setState({status:"ready",error:null}),"ready"):(setState({status:"ready"===state.status?"ready":"idle",error:null}),state.status)}catch(t){return reportError(e.updateId,messageOf(t)),"error"}finally{setState({lastCheckedAt:Date.now()}),checking=null}})(),checking};exports.checkForOtaUpdate=checkForOtaUpdate;const onAppStateChange=e=>{if("active"!==e||!1===config?.checkOnForeground)return;const t=config?.minCheckIntervalMs??DEFAULT_MIN_CHECK_INTERVAL_MS;Date.now()-lastCheckStartedAt<t||checkForOtaUpdate()},initOtaUpdates=e=>{const t=null===config;if(config=e,!t)return;const a=resolveUpdates();if(!a?.isEnabled)return void setState({status:"unavailable"});setState({status:"idle",runningUpdateId:a.isEmbeddedLaunch?null:a.updateId}),reportRunningUpdate(a).catch(e=>{_index2.Logger.info(`${PREFIX} start-up report failed: ${messageOf(e)}`)}),"function"==typeof a.addUpdatesStateChangeListener&&(stateSubscription=a.addUpdatesStateChangeListener(e=>onStateChange(e.context))),appStateSubscription=_reactNative.AppState.addEventListener("change",onAppStateChange);"ON_LOAD"===a.checkAutomatically||"WIFI_ONLY"===a.checkAutomatically||checkForOtaUpdate()};exports.initOtaUpdates=initOtaUpdates;const applyOtaUpdate=async()=>{const e=resolveUpdates();e?.isEnabled&&"ready"===state.status&&await e.reloadAsync()};exports.applyOtaUpdate=applyOtaUpdate;const stopOtaUpdates=()=>{stateSubscription?.remove(),appStateSubscription?.remove(),stateSubscription=null,appStateSubscription=null,config=null,checking=null,lastCheckStartedAt=0,lastReportedError=null,resolved=void 0,setState(INITIAL_STATE)};exports.stopOtaUpdates=stopOtaUpdates;const subscribe=e=>(subscribers.add(e),()=>{subscribers.delete(e)}),getSnapshot=()=>state,useOtaUpdates=()=>({...(0,_react.useSyncExternalStore)(subscribe,getSnapshot,getSnapshot),check:checkForOtaUpdate,apply:applyOtaUpdate});exports.useOtaUpdates=useOtaUpdates;
@@ -0,0 +1 @@
1
+ "use strict";export{applyOtaUpdate,checkForOtaUpdate,DEFAULT_MIN_CHECK_INTERVAL_MS,initOtaUpdates,normalizeAppIdentity,setOtaUpdatesImplementation,stopOtaUpdates,useOtaUpdates}from"./updates.js";
@@ -0,0 +1 @@
1
+ "use strict";import{useSyncExternalStore}from"react";import{AppState,Platform}from"react-native";import{getConfigValue}from"../config/index.js";import{Logger}from"../logger/index.js";import{Storage}from"../storage/index.js";const PREFIX="[@webority/mobile-core] OTA updates:",REPORTED_KEY="@webority/ota/last-reported-update-id",ENV_IDENTITY_SUFFIXES=[".staging",".development"];export const DEFAULT_MIN_CHECK_INTERVAL_MS=9e5;const EVENT_TIMEOUT_MS=1e4;let resolved,override=null;export const setOtaUpdatesImplementation=t=>{override=t};const resolveUpdates=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{resolved=require("expo-updates")}catch{resolved=null}return resolved},INITIAL_STATE={status:"unavailable",runningUpdateId:null,error:null,lastCheckedAt:null};let state=INITIAL_STATE;const subscribers=new Set,setState=t=>{state={...state,...t};for(const t of subscribers)t()};let config=null,stateSubscription=null,appStateSubscription=null,checking=null,lastCheckStartedAt=0,lastReportedError=null;const messageOf=t=>t instanceof Error?t.message:String(t);export const normalizeAppIdentity=t=>{const e=String(t??"").trim().toLowerCase(),r=ENV_IDENTITY_SUFFIXES.find(t=>e.endsWith(t));return r?e.slice(0,-r.length):e};const ownIdentity=()=>config?.appIdentifier??getConfigValue("appId",null)??null,identityMismatch=t=>{const e=t?.extra?.webority?.appIdentifier;if(!e)return null;const r=ownIdentity();return r&&normalizeAppIdentity(e)===normalizeAppIdentity(r)?null:`identity mismatch: the update is for '${e}' but this app is '${r??"unset"}'`},postEvent=async(t,e,r)=>{const a=config?.serverUrl;if(!a||!t)return;const n=new AbortController,s=setTimeout(()=>n.abort(),1e4);try{await fetch(`${a.replace(/\/+$/,"")}/ota/updates/${encodeURIComponent(t)}/events`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:e,platform:Platform.OS,runtimeVersion:resolveUpdates()?.runtimeVersion??null,message:r}),signal:n.signal})}catch(t){Logger.info(`${PREFIX} the ${e} event was not reported: ${messageOf(t)}`)}finally{clearTimeout(s)}},reportError=(t,e)=>{setState({status:"error",error:e});const r=`${t??""}|${e}`;r!==lastReportedError&&(lastReportedError=r,Logger.error(`${PREFIX} ${e}`),postEvent(t,"error",e))},reportRunningUpdate=async t=>{if(t.isEmbeddedLaunch||!t.updateId)return;const e=identityMismatch(t.manifest);e?reportError(t.updateId,e):await Storage.getItem(REPORTED_KEY)!==t.updateId&&(await postEvent(t.updateId,"success"),await Storage.setItem(REPORTED_KEY,t.updateId),Logger.info(`${PREFIX} running update ${t.updateId}`))},onStateChange=t=>{const e=t.downloadError??t.checkError;if(e){const r=t.latestManifest?.id??resolveUpdates()?.updateId;return void reportError(r,e.message)}t.isChecking?setState({status:"checking"}):t.isDownloading?setState({status:"downloading"}):t.isUpdatePending&&setState({status:"ready",error:null})};export const checkForOtaUpdate=()=>{const t=resolveUpdates();if(!config||!t?.isEnabled)return Promise.resolve("unavailable");if(checking)return checking;lastCheckStartedAt=Date.now();return checking=(async()=>{try{setState({status:"checking"});const e=await t.checkForUpdateAsync();if(e.isAvailable){const t=identityMismatch(e.manifest);if(t)return reportError(e.manifest?.id,t),"error"}return e.isAvailable||e.isRollBackToEmbedded?(setState({status:"downloading"}),await t.fetchUpdateAsync(),setState({status:"ready",error:null}),"ready"):(setState({status:"ready"===state.status?"ready":"idle",error:null}),state.status)}catch(e){return reportError(t.updateId,messageOf(e)),"error"}finally{setState({lastCheckedAt:Date.now()}),checking=null}})(),checking};const onAppStateChange=t=>{if("active"!==t||!1===config?.checkOnForeground)return;const e=config?.minCheckIntervalMs??9e5;Date.now()-lastCheckStartedAt<e||checkForOtaUpdate()};export const initOtaUpdates=t=>{const e=null===config;if(config=t,!e)return;const r=resolveUpdates();if(!r?.isEnabled)return void setState({status:"unavailable"});setState({status:"idle",runningUpdateId:r.isEmbeddedLaunch?null:r.updateId}),reportRunningUpdate(r).catch(t=>{Logger.info(`${PREFIX} start-up report failed: ${messageOf(t)}`)}),"function"==typeof r.addUpdatesStateChangeListener&&(stateSubscription=r.addUpdatesStateChangeListener(t=>onStateChange(t.context))),appStateSubscription=AppState.addEventListener("change",onAppStateChange);"ON_LOAD"===r.checkAutomatically||"WIFI_ONLY"===r.checkAutomatically||checkForOtaUpdate()};export const applyOtaUpdate=async()=>{const t=resolveUpdates();t?.isEnabled&&"ready"===state.status&&await t.reloadAsync()};export const stopOtaUpdates=()=>{stateSubscription?.remove(),appStateSubscription?.remove(),stateSubscription=null,appStateSubscription=null,config=null,checking=null,lastCheckStartedAt=0,lastReportedError=null,resolved=void 0,setState(INITIAL_STATE)};const subscribe=t=>(subscribers.add(t),()=>{subscribers.delete(t)}),getSnapshot=()=>state;export const useOtaUpdates=()=>({...useSyncExternalStore(subscribe,getSnapshot,getSnapshot),check:checkForOtaUpdate,apply:applyOtaUpdate});
@@ -12,6 +12,7 @@
12
12
  * @webority/mobile-core/download a filesystem module
13
13
  * @webority/mobile-core/share expo-sharing
14
14
  * @webority/mobile-core/secureScreen expo-screen-capture
15
+ * @webority/mobile-core/updates expo-updates
15
16
  * @webority/mobile-core/smsRetriever Android only, own TurboModule
16
17
  * @webority/mobile-core/query RTK Query base query and createApi preset
17
18
  * (@reduxjs/toolkit and react-redux)
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Over-the-air updates from the Webority OS update server through expo-updates. A separate entry
3
+ * point because it names expo-updates, an optional peer: Metro resolves every require reachable
4
+ * from an import, so keeping it off the main barrel lets an app without OTA skip the package.
5
+ */
6
+ export type { OtaUpdatesConfig, OtaUpdatesState, OtaUpdatesStatus, UpdatesModuleLike } from './updates';
7
+ export { applyOtaUpdate, checkForOtaUpdate, DEFAULT_MIN_CHECK_INTERVAL_MS, initOtaUpdates, normalizeAppIdentity, setOtaUpdatesImplementation, stopOtaUpdates, useOtaUpdates } from './updates';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,105 @@
1
+ export declare const DEFAULT_MIN_CHECK_INTERVAL_MS: number;
2
+ interface ManifestLike {
3
+ id?: string;
4
+ extra?: {
5
+ webority?: {
6
+ appIdentifier?: string | null;
7
+ } | null;
8
+ } | null;
9
+ }
10
+ interface StateContextLike {
11
+ isChecking?: boolean;
12
+ isDownloading?: boolean;
13
+ isUpdatePending?: boolean;
14
+ latestManifest?: ManifestLike;
15
+ downloadedManifest?: ManifestLike;
16
+ checkError?: Error;
17
+ downloadError?: Error;
18
+ }
19
+ /** The slice of expo-updates this module calls, so the optional peer stays untyped at build. */
20
+ export interface UpdatesModuleLike {
21
+ isEnabled: boolean;
22
+ isEmbeddedLaunch: boolean;
23
+ updateId: string | null;
24
+ runtimeVersion: string | null;
25
+ checkAutomatically: string | null;
26
+ manifest: ManifestLike | null | undefined;
27
+ checkForUpdateAsync: () => Promise<{
28
+ isAvailable: boolean;
29
+ isRollBackToEmbedded: boolean;
30
+ manifest?: ManifestLike;
31
+ }>;
32
+ fetchUpdateAsync: () => Promise<{
33
+ isNew: boolean;
34
+ isRollBackToEmbedded: boolean;
35
+ }>;
36
+ reloadAsync: () => Promise<void>;
37
+ addUpdatesStateChangeListener?: (listener: (event: {
38
+ context: StateContextLike;
39
+ }) => void) => {
40
+ remove: () => void;
41
+ };
42
+ }
43
+ export interface OtaUpdatesConfig {
44
+ /**
45
+ * The update server's origin, from the app's compiled settings (`OTA_UPDATE_BASE_URL`). Update
46
+ * events are posted to `<serverUrl>/ota/updates/<updateId>/events`.
47
+ */
48
+ serverUrl: string;
49
+ /**
50
+ * The app's own identity, compared with the identity stamped into every served update. Defaults
51
+ * to `appId` from `setConfig`, the base package name / bundle id from the compiled settings.
52
+ */
53
+ appIdentifier?: string;
54
+ /** Check again each time the app returns to the foreground. Default true. */
55
+ checkOnForeground?: boolean;
56
+ /** The shortest gap between two checks this module starts, in milliseconds. Default 15 minutes. */
57
+ minCheckIntervalMs?: number;
58
+ }
59
+ /**
60
+ * `unavailable`: OTA is off in this build (a debug build, or expo-updates not installed).
61
+ * `ready`: an update is downloaded and applies on the next cold start, or at `applyOtaUpdate()`.
62
+ */
63
+ export type OtaUpdatesStatus = 'unavailable' | 'idle' | 'checking' | 'downloading' | 'ready' | 'error';
64
+ export interface OtaUpdatesState {
65
+ status: OtaUpdatesStatus;
66
+ /** The update now running; null for the bundle embedded in the store build. */
67
+ runningUpdateId: string | null;
68
+ /** The last check or download failure, or an identity mismatch; null after a clean check. */
69
+ error: string | null;
70
+ /** When this module last finished a check, in epoch milliseconds. */
71
+ lastCheckedAt: number | null;
72
+ }
73
+ /**
74
+ * Replace expo-updates. For tests and the showcase; pass `null` to restore the real module.
75
+ */
76
+ export declare const setOtaUpdatesImplementation: (impl: UpdatesModuleLike | null) => void;
77
+ /** Trims, lower-cases and strips one environment suffix, as the server does. */
78
+ export declare const normalizeAppIdentity: (id: string | null | undefined) => string;
79
+ /**
80
+ * Asks the server for a newer update and downloads it; it applies on the next cold start, or at
81
+ * `applyOtaUpdate()`. An update stamped for another app is refused before download. Resolves with
82
+ * the status the check ended in; a check already running is joined, not repeated.
83
+ */
84
+ export declare const checkForOtaUpdate: () => Promise<OtaUpdatesStatus>;
85
+ /**
86
+ * Starts OTA updates for this launch: reports the running update to the server (success once per
87
+ * update, or an error when it was built for another app), reports check and download failures,
88
+ * checks at launch unless expo-updates already does, and again on each return to the foreground.
89
+ * Call it once at start-up; a later call only replaces the config. A no-op where OTA is off.
90
+ */
91
+ export declare const initOtaUpdates: (next: OtaUpdatesConfig) => void;
92
+ /** Restarts the app on a downloaded update now, rather than at the next cold start. */
93
+ export declare const applyOtaUpdate: () => Promise<void>;
94
+ /** Removes the listeners and forgets the config, so `initOtaUpdates` starts over. For tests. */
95
+ export declare const stopOtaUpdates: () => void;
96
+ /**
97
+ * The update state for a screen that shows it, for example a "restart to update" banner when
98
+ * `status` is `ready`. Most apps show nothing and let the update apply on the next cold start.
99
+ */
100
+ export declare const useOtaUpdates: () => OtaUpdatesState & {
101
+ check: () => Promise<OtaUpdatesStatus>;
102
+ apply: () => Promise<void>;
103
+ };
104
+ export {};
105
+ //# sourceMappingURL=updates.d.ts.map
@@ -12,6 +12,7 @@
12
12
  * @webority/mobile-core/download a filesystem module
13
13
  * @webority/mobile-core/share expo-sharing
14
14
  * @webority/mobile-core/secureScreen expo-screen-capture
15
+ * @webority/mobile-core/updates expo-updates
15
16
  * @webority/mobile-core/smsRetriever Android only, own TurboModule
16
17
  * @webority/mobile-core/query RTK Query base query and createApi preset
17
18
  * (@reduxjs/toolkit and react-redux)
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Over-the-air updates from the Webority OS update server through expo-updates. A separate entry
3
+ * point because it names expo-updates, an optional peer: Metro resolves every require reachable
4
+ * from an import, so keeping it off the main barrel lets an app without OTA skip the package.
5
+ */
6
+ export type { OtaUpdatesConfig, OtaUpdatesState, OtaUpdatesStatus, UpdatesModuleLike } from './updates.js';
7
+ export { applyOtaUpdate, checkForOtaUpdate, DEFAULT_MIN_CHECK_INTERVAL_MS, initOtaUpdates, normalizeAppIdentity, setOtaUpdatesImplementation, stopOtaUpdates, useOtaUpdates } from './updates.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,105 @@
1
+ export declare const DEFAULT_MIN_CHECK_INTERVAL_MS: number;
2
+ interface ManifestLike {
3
+ id?: string;
4
+ extra?: {
5
+ webority?: {
6
+ appIdentifier?: string | null;
7
+ } | null;
8
+ } | null;
9
+ }
10
+ interface StateContextLike {
11
+ isChecking?: boolean;
12
+ isDownloading?: boolean;
13
+ isUpdatePending?: boolean;
14
+ latestManifest?: ManifestLike;
15
+ downloadedManifest?: ManifestLike;
16
+ checkError?: Error;
17
+ downloadError?: Error;
18
+ }
19
+ /** The slice of expo-updates this module calls, so the optional peer stays untyped at build. */
20
+ export interface UpdatesModuleLike {
21
+ isEnabled: boolean;
22
+ isEmbeddedLaunch: boolean;
23
+ updateId: string | null;
24
+ runtimeVersion: string | null;
25
+ checkAutomatically: string | null;
26
+ manifest: ManifestLike | null | undefined;
27
+ checkForUpdateAsync: () => Promise<{
28
+ isAvailable: boolean;
29
+ isRollBackToEmbedded: boolean;
30
+ manifest?: ManifestLike;
31
+ }>;
32
+ fetchUpdateAsync: () => Promise<{
33
+ isNew: boolean;
34
+ isRollBackToEmbedded: boolean;
35
+ }>;
36
+ reloadAsync: () => Promise<void>;
37
+ addUpdatesStateChangeListener?: (listener: (event: {
38
+ context: StateContextLike;
39
+ }) => void) => {
40
+ remove: () => void;
41
+ };
42
+ }
43
+ export interface OtaUpdatesConfig {
44
+ /**
45
+ * The update server's origin, from the app's compiled settings (`OTA_UPDATE_BASE_URL`). Update
46
+ * events are posted to `<serverUrl>/ota/updates/<updateId>/events`.
47
+ */
48
+ serverUrl: string;
49
+ /**
50
+ * The app's own identity, compared with the identity stamped into every served update. Defaults
51
+ * to `appId` from `setConfig`, the base package name / bundle id from the compiled settings.
52
+ */
53
+ appIdentifier?: string;
54
+ /** Check again each time the app returns to the foreground. Default true. */
55
+ checkOnForeground?: boolean;
56
+ /** The shortest gap between two checks this module starts, in milliseconds. Default 15 minutes. */
57
+ minCheckIntervalMs?: number;
58
+ }
59
+ /**
60
+ * `unavailable`: OTA is off in this build (a debug build, or expo-updates not installed).
61
+ * `ready`: an update is downloaded and applies on the next cold start, or at `applyOtaUpdate()`.
62
+ */
63
+ export type OtaUpdatesStatus = 'unavailable' | 'idle' | 'checking' | 'downloading' | 'ready' | 'error';
64
+ export interface OtaUpdatesState {
65
+ status: OtaUpdatesStatus;
66
+ /** The update now running; null for the bundle embedded in the store build. */
67
+ runningUpdateId: string | null;
68
+ /** The last check or download failure, or an identity mismatch; null after a clean check. */
69
+ error: string | null;
70
+ /** When this module last finished a check, in epoch milliseconds. */
71
+ lastCheckedAt: number | null;
72
+ }
73
+ /**
74
+ * Replace expo-updates. For tests and the showcase; pass `null` to restore the real module.
75
+ */
76
+ export declare const setOtaUpdatesImplementation: (impl: UpdatesModuleLike | null) => void;
77
+ /** Trims, lower-cases and strips one environment suffix, as the server does. */
78
+ export declare const normalizeAppIdentity: (id: string | null | undefined) => string;
79
+ /**
80
+ * Asks the server for a newer update and downloads it; it applies on the next cold start, or at
81
+ * `applyOtaUpdate()`. An update stamped for another app is refused before download. Resolves with
82
+ * the status the check ended in; a check already running is joined, not repeated.
83
+ */
84
+ export declare const checkForOtaUpdate: () => Promise<OtaUpdatesStatus>;
85
+ /**
86
+ * Starts OTA updates for this launch: reports the running update to the server (success once per
87
+ * update, or an error when it was built for another app), reports check and download failures,
88
+ * checks at launch unless expo-updates already does, and again on each return to the foreground.
89
+ * Call it once at start-up; a later call only replaces the config. A no-op where OTA is off.
90
+ */
91
+ export declare const initOtaUpdates: (next: OtaUpdatesConfig) => void;
92
+ /** Restarts the app on a downloaded update now, rather than at the next cold start. */
93
+ export declare const applyOtaUpdate: () => Promise<void>;
94
+ /** Removes the listeners and forgets the config, so `initOtaUpdates` starts over. For tests. */
95
+ export declare const stopOtaUpdates: () => void;
96
+ /**
97
+ * The update state for a screen that shows it, for example a "restart to update" banner when
98
+ * `status` is `ready`. Most apps show nothing and let the update apply on the next cold start.
99
+ */
100
+ export declare const useOtaUpdates: () => OtaUpdatesState & {
101
+ check: () => Promise<OtaUpdatesStatus>;
102
+ apply: () => Promise<void>;
103
+ };
104
+ export {};
105
+ //# sourceMappingURL=updates.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@webority/mobile-core",
3
- "version": "0.4.1",
4
- "description": "Platform layer for Webority React Native apps: HTTP clients, auth/token storage, config, logging, formatters, validators, network status, permissions, storage and version checks. No UI.",
3
+ "version": "0.5.0",
4
+ "description": "Platform layer for Webority React Native apps: HTTP clients, auth/token storage, config, logging, formatters, validators, network status, permissions, storage, version checks and over-the-air updates. No UI.",
5
5
  "keywords": [
6
6
  "react-native",
7
7
  "auth",
@@ -11,6 +11,7 @@
11
11
  "network",
12
12
  "permissions",
13
13
  "storage",
14
+ "ota",
14
15
  "webority"
15
16
  ],
16
17
  "homepage": "https://www.webority.com",
@@ -231,6 +232,16 @@
231
232
  "default": "./lib/commonjs/redux/index.js"
232
233
  }
233
234
  },
235
+ "./updates": {
236
+ "import": {
237
+ "types": "./lib/typescript/module/updates/index.d.ts",
238
+ "default": "./lib/module/updates/index.js"
239
+ },
240
+ "require": {
241
+ "types": "./lib/typescript/commonjs/updates/index.d.ts",
242
+ "default": "./lib/commonjs/updates/index.js"
243
+ }
244
+ },
234
245
  "./package.json": "./package.json"
235
246
  },
236
247
  "main": "./lib/commonjs/index.js",
@@ -258,6 +269,7 @@
258
269
  }
259
270
  },
260
271
  "scripts": {
272
+ "prepack": "node ../../scripts/package-guard-pack.mjs --output-dirs lib --allow-file ../../.webority/package-allow.json",
261
273
  "build": "bob build && node ../../scripts/minify-lib.mjs",
262
274
  "clean": "rimraf lib",
263
275
  "lint": "biome lint ./src",
@@ -288,6 +300,7 @@
288
300
  "expo-sharing": ">=57.0.0",
289
301
  "expo-sqlite": ">=57.0.0",
290
302
  "expo-task-manager": ">=57.0.0",
303
+ "expo-updates": ">=57.0.0",
291
304
  "react": "^19.1.0",
292
305
  "react-native": ">=0.81.0",
293
306
  "react-redux": "^9.3.0"
@@ -332,6 +345,9 @@
332
345
  "expo-task-manager": {
333
346
  "optional": true
334
347
  },
348
+ "expo-updates": {
349
+ "optional": true
350
+ },
335
351
  "react-redux": {
336
352
  "optional": true
337
353
  }