@webority/mobile-core 0.4.1 → 0.6.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.
Files changed (30) hide show
  1. package/README.md +15 -0
  2. package/lib/commonjs/api/clientHeaders.js +1 -1
  3. package/lib/commonjs/api/minAppVersion.js +1 -1
  4. package/lib/commonjs/review/index.js +1 -0
  5. package/lib/commonjs/updates/index.js +1 -0
  6. package/lib/commonjs/updates/updates.js +1 -0
  7. package/lib/commonjs/versionCheck/appBuild.js +1 -0
  8. package/lib/commonjs/versionCheck/index.js +1 -1
  9. package/lib/module/api/clientHeaders.js +1 -1
  10. package/lib/module/api/minAppVersion.js +1 -1
  11. package/lib/module/review/index.js +1 -0
  12. package/lib/module/updates/index.js +1 -0
  13. package/lib/module/updates/updates.js +1 -0
  14. package/lib/module/versionCheck/appBuild.js +1 -0
  15. package/lib/module/versionCheck/index.js +1 -1
  16. package/lib/typescript/commonjs/api/clientHeaders.d.ts +5 -1
  17. package/lib/typescript/commonjs/index.d.ts +2 -0
  18. package/lib/typescript/commonjs/review/index.d.ts +42 -0
  19. package/lib/typescript/commonjs/updates/index.d.ts +8 -0
  20. package/lib/typescript/commonjs/updates/updates.d.ts +105 -0
  21. package/lib/typescript/commonjs/versionCheck/appBuild.d.ts +30 -0
  22. package/lib/typescript/commonjs/versionCheck/index.d.ts +2 -15
  23. package/lib/typescript/module/api/clientHeaders.d.ts +5 -1
  24. package/lib/typescript/module/index.d.ts +2 -0
  25. package/lib/typescript/module/review/index.d.ts +42 -0
  26. package/lib/typescript/module/updates/index.d.ts +8 -0
  27. package/lib/typescript/module/updates/updates.d.ts +105 -0
  28. package/lib/typescript/module/versionCheck/appBuild.d.ts +30 -0
  29. package/lib/typescript/module/versionCheck/index.d.ts +2 -15
  30. package/package.json +32 -2
package/README.md CHANGED
@@ -29,9 +29,11 @@ import { Geolocation } from '@webority/mobile-core/geolocation';
29
29
  import { openDatabase } from '@webority/mobile-core/sqlite';
30
30
  import { downloadFile } from '@webority/mobile-core/download';
31
31
  import { shareFile } from '@webority/mobile-core/share';
32
+ import { requestReviewAfterSuccess } from '@webority/mobile-core/review';
32
33
  import { initPush, enablePush, usePush } from '@webority/mobile-core/push';
33
34
  import { definePushActionTask } from '@webority/mobile-core/push/task';
34
35
  import { compressImage } from '@webority/mobile-core/compressor';
36
+ import { initOtaUpdates, useOtaUpdates } from '@webority/mobile-core/updates';
35
37
  import { createApiBaseQuery, createAppApi, setupApiListeners } from '@webority/mobile-core/query';
36
38
  ```
37
39
 
@@ -163,6 +165,19 @@ the listener and the Android task can each report it) runs once.
163
165
  Storage and network status stay on the main barrel but load no package on their own. Wire them at
164
166
  startup with `setStorageImplementation` and `setNetworkStatusImplementation` if you want them.
165
167
 
168
+ ## Build number header and forced updates
169
+
170
+ Both HTTP clients send `X-App-Build: <integer>` on every request: the app's whole build number
171
+ (`VERSION_INTERNAL`, Android versionCode, iOS CFBundleVersion). The API compares it with its minimum
172
+ and answers an older build with 426 `client_update_required`. The number comes from
173
+ `initWebority({ appBuild })` (`setConfig({ appBuild })`), else from expo-application's
174
+ `nativeBuildVersion`. Without a whole number the header is left off, and the server never refuses a
175
+ request without it. `X-Client-Build` (`<appVersion>+<commit>`) is unchanged.
176
+
177
+ A 426 `client_update_required` opens the same force-update flow as the `X-Min-App-Version` response
178
+ header: `useVersionCheck` turns `isUpdateRequired` on. The floor is the header when the reply has
179
+ one, else the body's `minVersion`, else one above this build. A 426 is never retried.
180
+
166
181
  ## Quiet requests and log redaction
167
182
 
168
183
  Both HTTP clients log a failed response through `Logger.error`. For a call whose failure the app
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getClientBuild=exports.applyClientHeaders=exports.applyAccountIdHeader=exports.CLIENT_KIND_HEADER=exports.CLIENT_KIND=exports.CLIENT_BUILD_HEADER=exports.ACCOUNT_ID_HEADER=void 0;var _index=require("../config/index.js");const CLIENT_BUILD_HEADER=exports.CLIENT_BUILD_HEADER="X-Client-Build",CLIENT_KIND_HEADER=exports.CLIENT_KIND_HEADER="X-Client-Kind",ACCOUNT_ID_HEADER=exports.ACCOUNT_ID_HEADER="X-Account-Id",CLIENT_KIND=exports.CLIENT_KIND="MobileApp",getClientBuild=()=>{const e=(0,_index.getConfigValue)("appVersion");if(!e)return null;const t=(0,_index.getConfigValue)("commit");return t?`${e}+${t}`:e};exports.getClientBuild=getClientBuild;const setIfAbsent=(e,t,s)=>{s&&!e.has(t)&&e.set(t,s)},applyClientHeaders=e=>{e.interceptors.request.use(e=>(setIfAbsent(e.headers,CLIENT_KIND_HEADER,CLIENT_KIND),setIfAbsent(e.headers,CLIENT_BUILD_HEADER,getClientBuild()),e))};exports.applyClientHeaders=applyClientHeaders;const applyAccountIdHeader=async(e,t)=>{const s=t?await t():null;s&&e.set(ACCOUNT_ID_HEADER,s)};exports.applyAccountIdHeader=applyAccountIdHeader;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.getClientBuild=exports.applyClientHeaders=exports.applyAccountIdHeader=exports.CLIENT_KIND_HEADER=exports.CLIENT_KIND=exports.CLIENT_BUILD_HEADER=exports.APP_BUILD_HEADER=exports.ACCOUNT_ID_HEADER=void 0;var _index=require("../config/index.js"),_appBuild=require("../versionCheck/appBuild.js");const CLIENT_BUILD_HEADER=exports.CLIENT_BUILD_HEADER="X-Client-Build",APP_BUILD_HEADER=exports.APP_BUILD_HEADER="X-App-Build",CLIENT_KIND_HEADER=exports.CLIENT_KIND_HEADER="X-Client-Kind",ACCOUNT_ID_HEADER=exports.ACCOUNT_ID_HEADER="X-Account-Id",CLIENT_KIND=exports.CLIENT_KIND="MobileApp",getClientBuild=()=>{const e=(0,_index.getConfigValue)("appVersion");if(!e)return null;const t=(0,_index.getConfigValue)("commit");return t?`${e}+${t}`:e};exports.getClientBuild=getClientBuild;const setIfAbsent=(e,t,s)=>{s&&!e.has(t)&&e.set(t,s)},applyClientHeaders=e=>{e.interceptors.request.use(e=>{setIfAbsent(e.headers,CLIENT_KIND_HEADER,CLIENT_KIND),setIfAbsent(e.headers,CLIENT_BUILD_HEADER,getClientBuild());const t=(0,_appBuild.readAppBuild)();return setIfAbsent(e.headers,APP_BUILD_HEADER,null===t?null:String(t)),e})};exports.applyClientHeaders=applyClientHeaders;const applyAccountIdHeader=async(e,t)=>{const s=t?await t():null;s&&e.set(ACCOUNT_ID_HEADER,s)};exports.applyAccountIdHeader=applyAccountIdHeader;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.readMinAppVersion=exports.getMinAppVersion=exports.addMinAppVersionListener=exports.MIN_APP_VERSION_HEADER=void 0;var _index=require("../logger/index.js");const MIN_APP_VERSION_HEADER=exports.MIN_APP_VERSION_HEADER="X-Min-App-Version";let current=null;const listeners=new Set,getMinAppVersion=()=>current;exports.getMinAppVersion=getMinAppVersion;const addMinAppVersionListener=e=>(listeners.add(e),null!==current&&e(current),()=>{listeners.delete(e)});exports.addMinAppVersionListener=addMinAppVersionListener;const readHeader=e=>{if(!e||"object"!=typeof e)return;const r=e.get;return"function"==typeof r?r.call(e,MIN_APP_VERSION_HEADER):e[MIN_APP_VERSION_HEADER.toLowerCase()]},record=e=>{const r=readHeader(e?.headers);if(null==r||""===r)return;const n=String(r).trim();if(!/^\d+$/.test(n))return void _index.Logger.warn(`[minAppVersion] ignored a non-integer ${MIN_APP_VERSION_HEADER}: ${n}`);const t=Number.parseInt(n,10);if(t!==current){current=t;for(const e of[...listeners])try{e(t)}catch(e){_index.Logger.error("[minAppVersion] a listener threw",e)}}},readMinAppVersion=e=>{e.interceptors.response.use(e=>(record(e),e),e=>(record(e?.response),Promise.reject(e)))};exports.readMinAppVersion=readMinAppVersion;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.readMinAppVersion=exports.getMinAppVersion=exports.addMinAppVersionListener=exports.MIN_APP_VERSION_HEADER=void 0;var _index=require("../logger/index.js"),_appBuild=require("../versionCheck/appBuild.js"),_buildNumber=require("../versionCheck/buildNumber.js");const MIN_APP_VERSION_HEADER=exports.MIN_APP_VERSION_HEADER="X-Min-App-Version";let current=null;const listeners=new Set,getMinAppVersion=()=>current;exports.getMinAppVersion=getMinAppVersion;const addMinAppVersionListener=e=>(listeners.add(e),null!==current&&e(current),()=>{listeners.delete(e)});exports.addMinAppVersionListener=addMinAppVersionListener;const readHeader=e=>{if(!e||"object"!=typeof e)return;const r=e.get;return"function"==typeof r?r.call(e,MIN_APP_VERSION_HEADER):e[MIN_APP_VERSION_HEADER.toLowerCase()]},setFloor=e=>{if(e!==current){current=e;for(const r of[...listeners])try{r(e)}catch(e){_index.Logger.error("[minAppVersion] a listener threw",e)}}},record=e=>{const r=readHeader(e?.headers);if(null==r||""===r)return;const n=String(r).trim();/^\d+$/.test(n)?setFloor(Number.parseInt(n,10)):_index.Logger.warn(`[minAppVersion] ignored a non-integer ${MIN_APP_VERSION_HEADER}: ${n}`)},bodyField=(e,r)=>e&&"object"==typeof e?e[r]:void 0,recordUpdateRequired=e=>{if(426!==e?.status||"client_update_required"!==bodyField(e.data,"code"))return;const r=(0,_appBuild.readAppBuild)(),n=(0,_buildNumber.parseBuildNumber)(bodyField(e.data,"minVersion"))??(null===r?null:r+1);null!==n&&setFloor(Math.max(current??0,n))},readMinAppVersion=e=>{e.interceptors.response.use(e=>(record(e),e),e=>{const r=e?.response;return record(r),recordUpdateRequired(r),Promise.reject(e)})};exports.readMinAppVersion=readMinAppVersion;
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.requestReviewAfterSuccess=exports.configureReviewPrompt=exports.REVIEW_PROMPT_STORAGE_KEY=exports.DEFAULT_REVIEW_COOL_DOWN_DAYS=void 0;var _index=require("../logger/index.js"),_index2=require("../storage/index.js");const TAG="[reviewPrompt]",DAY_MS=864e5,DEFAULT_REVIEW_COOL_DOWN_DAYS=exports.DEFAULT_REVIEW_COOL_DOWN_DAYS=90,REVIEW_PROMPT_STORAGE_KEY=exports.REVIEW_PROMPT_STORAGE_KEY="@webority/review-prompt",storeReview=()=>require("expo-store-review");let coolDownDays=DEFAULT_REVIEW_COOL_DOWN_DAYS,inFlight=!1;const configureReviewPrompt=(e={})=>{const o=e.coolDownDays??DEFAULT_REVIEW_COOL_DOWN_DAYS;if(!Number.isFinite(o)||o<0)throw new Error(`[@webority/mobile-core] configureReviewPrompt: coolDownDays must be a number of days, 0 or more; got ${String(o)}.`);o<DEFAULT_REVIEW_COOL_DOWN_DAYS&&_index.Logger.warn(`${TAG} coolDownDays ${o} is under the ${DEFAULT_REVIEW_COOL_DOWN_DAYS}-day minimum for store builds.`),coolDownDays=o};exports.configureReviewPrompt=configureReviewPrompt;const withinCoolDown=(e,o)=>{if(!e)return!1;const r=Date.parse(e.lastAskedUtc);return!Number.isNaN(r)&&o-r<coolDownDays*DAY_MS},storeCanPrompt=async()=>{try{const e=storeReview();return await e.isAvailableAsync()&&await e.hasAction()}catch(e){return _index.Logger.warn(`${TAG} the store review module is not available`,e),!1}},requestReviewAfterSuccess=async e=>{if(inFlight)return"skipped-cooldown";inFlight=!0;try{const o=await _index2.Storage.getJson(REVIEW_PROMPT_STORAGE_KEY),r=Date.now();if(withinCoolDown(o,r))return"skipped-cooldown";if(!await storeCanPrompt())return"skipped-unavailable";try{await storeReview().requestReview()}catch(o){return _index.Logger.warn(`${TAG} the store review request failed after "${e}"`,o),"skipped-unavailable"}const t={lastAskedUtc:new Date(r).toISOString(),moment:e};return await _index2.Storage.setJson(REVIEW_PROMPT_STORAGE_KEY,t)||_index.Logger.warn(`${TAG} the prompt time could not be stored, so the cool-down may not hold`),_index.Logger.info(`${TAG} review prompt requested after "${e}"`),"shown"}finally{inFlight=!1}};exports.requestReviewAfterSuccess=requestReviewAfterSuccess;
@@ -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";Object.defineProperty(exports,"__esModule",{value:!0}),exports.setDeviceInfo=exports.readAppBuild=exports.getDeviceInfo=exports.expoApplicationBackend=void 0;var Application=_interopRequireWildcard(require("expo-application")),_index=require("../config/index.js"),_buildNumber=require("./buildNumber.js");function _interopRequireWildcard(e,i){if("function"==typeof WeakMap)var t=new WeakMap,n=new WeakMap;return(_interopRequireWildcard=function(e,i){if(!i&&e&&e.__esModule)return e;var r,o,p={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return p;if(r=i?n:t){if(r.has(e))return r.get(e);r.set(e,p)}for(const i in e)"default"!==i&&{}.hasOwnProperty.call(e,i)&&((o=(r=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,i))&&(o.get||o.set)?r(p,i,o):p[i]=e[i]);return p})(e,i)}const expoApplicationBackend=exports.expoApplicationBackend={getBundleId:()=>Application.applicationId??"",getVersion:()=>Application.nativeApplicationVersion??"",getBuildNumber:()=>Application.nativeBuildVersion??""};let deviceInfo=null;const setDeviceInfo=e=>{deviceInfo=e};exports.setDeviceInfo=setDeviceInfo;const getDeviceInfo=()=>deviceInfo;exports.getDeviceInfo=getDeviceInfo;const readAppBuild=()=>{const e=(0,_buildNumber.parseBuildNumber)((0,_index.getConfigValue)("appBuild"));if(null!==e)return e;try{return(0,_buildNumber.parseBuildNumber)(deviceInfo?.getBuildNumber?.()??expoApplicationBackend.getBuildNumber())}catch{return null}};exports.readAppBuild=readAppBuild;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=exports.VersionCheck=void 0,Object.defineProperty(exports,"isBuildBelowMinimum",{enumerable:!0,get:function(){return _buildNumber.isBuildBelowMinimum}}),Object.defineProperty(exports,"parseBuildNumber",{enumerable:!0,get:function(){return _buildNumber.parseBuildNumber}}),exports.useVersionCheck=exports.setDeviceInfoImplementation=void 0;var Application=_interopRequireWildcard(require("expo-application")),_react=require("react"),_minAppVersion=require("../api/minAppVersion.js"),_index=require("../config/index.js"),_index2=require("../logger/index.js"),_buildNumber=require("./buildNumber.js");function _interopRequireWildcard(e,r){if("function"==typeof WeakMap)var i=new WeakMap,t=new WeakMap;return(_interopRequireWildcard=function(e,r){if(!r&&e&&e.__esModule)return e;var n,o,l={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return l;if(n=r?t:i){if(n.has(e))return n.get(e);n.set(e,l)}for(const r in e)"default"!==r&&{}.hasOwnProperty.call(e,r)&&((o=(n=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,r))&&(o.get||o.set)?n(l,r,o):l[r]=e[r]);return l})(e,r)}const expoApplicationBackend={getBundleId:()=>Application.applicationId??"",getVersion:()=>Application.nativeApplicationVersion??"",getBuildNumber:()=>Application.nativeBuildVersion??""};let deviceInfo=null;const setDeviceInfoImplementation=e=>{deviceInfo=e};exports.setDeviceInfoImplementation=setDeviceInfoImplementation;const missing=(e,r)=>{const i=new Error(`[VersionCheck] the app's ${e} is not available. Call setConfig({ ${r}: ... }) — every Webority app already has this value in appsettings.Compiled.json — or setDeviceInfoImplementation() with a native provider.`);return _index2.Logger.error(i.message),i},getSystemName=()=>{const{Platform:e}=require("react-native");return e.OS},getBundleId=()=>{const e=(0,_index.getConfigValue)("appId");if(e)return e;const r=(deviceInfo??expoApplicationBackend).getBundleId();if(r)return r;throw missing("store identifier","appId")},getBuildNumber=()=>{const e=(0,_buildNumber.parseBuildNumber)((0,_index.getConfigValue)("appBuild"));if(null!==e)return e;const r=(0,_buildNumber.parseBuildNumber)(deviceInfo?.getBuildNumber?.()??expoApplicationBackend.getBuildNumber());if(null!==r)return r;throw missing("build number","appBuild")},getCountry=()=>(0,_index.getConfigValue)("country","in").toLowerCase(),subscribeMinimum=e=>(0,_minAppVersion.addMinAppVersionListener)(()=>e()),getFallbackStoreUrl=()=>{const e=getSystemName().toLowerCase(),r=getCountry();if("ios"===e){const e=(0,_index.getConfigValue)("appStoreId");return e?`https://apps.apple.com/${r}/app/id${e}`:`https://apps.apple.com/${r}/app/${getBundleId()}`}return`https://play.google.com/store/apps/details?id=${getBundleId()}&hl=${r}`},storeUrlOrNull=()=>{try{return getFallbackStoreUrl()}catch{return null}},checkForUpdates=()=>{const e=(0,_buildNumber.isBuildBelowMinimum)(getBuildNumber(),(0,_minAppVersion.getMinAppVersion)());return{isNewVersionAvailable:e,updateType:e?"major":null,url:e?storeUrlOrNull():null}},VersionCheck=exports.VersionCheck={isNewVersionAvailable:async()=>checkForUpdates(),openUpdateUrl:async e=>{const{Linking:r}=require("react-native");try{if(e)_index2.Logger.info(`Opening app store URL: ${e}`),await r.openURL(e);else{const e=getFallbackStoreUrl();_index2.Logger.info(`Opening fallback store URL: ${e}`),await r.openURL(e)}return!0}catch(i){const t=i instanceof Error?i.message:String(i);_index2.Logger.error(`Failed to open store URL: ${t}`);try{if(e){const e=getFallbackStoreUrl();return _index2.Logger.info(`Trying fallback store URL: ${e}`),await r.openURL(e),!0}}catch(e){const r=e instanceof Error?e.message:String(e);_index2.Logger.error(`Failed to open fallback URL: ${r}`)}return!1}},getFallbackStoreUrl:getFallbackStoreUrl},NOT_REQUIRED={isUpdateRequired:!1,updateUrl:null},useVersionCheck=(e,r=(0,_index.getConfigValue)("environment","Development"))=>{const i=(0,_react.useSyncExternalStore)(subscribeMinimum,_minAppVersion.getMinAppVersion,_minAppVersion.getMinAppVersion);return(0,_react.useMemo)(()=>{if(!e||"Production"!==r||null===i)return NOT_REQUIRED;try{if(!(0,_buildNumber.isBuildBelowMinimum)(getBuildNumber(),i))return NOT_REQUIRED}catch{return NOT_REQUIRED}return{isUpdateRequired:!0,updateUrl:storeUrlOrNull()}},[e,r,i])};exports.useVersionCheck=useVersionCheck;var _default=exports.default=VersionCheck;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=exports.VersionCheck=void 0,Object.defineProperty(exports,"isBuildBelowMinimum",{enumerable:!0,get:function(){return _buildNumber.isBuildBelowMinimum}}),Object.defineProperty(exports,"parseBuildNumber",{enumerable:!0,get:function(){return _buildNumber.parseBuildNumber}}),Object.defineProperty(exports,"readAppBuild",{enumerable:!0,get:function(){return _appBuild.readAppBuild}}),exports.useVersionCheck=exports.setDeviceInfoImplementation=void 0;var _react=require("react"),_minAppVersion=require("../api/minAppVersion.js"),_index=require("../config/index.js"),_index2=require("../logger/index.js"),_appBuild=require("./appBuild.js"),_buildNumber=require("./buildNumber.js");const setDeviceInfoImplementation=e=>{(0,_appBuild.setDeviceInfo)(e)};exports.setDeviceInfoImplementation=setDeviceInfoImplementation;const missing=(e,r)=>{const t=new Error(`[VersionCheck] the app's ${e} is not available. Call setConfig({ ${r}: ... }) — every Webority app already has this value in appsettings.Compiled.json — or setDeviceInfoImplementation() with a native provider.`);return _index2.Logger.error(t.message),t},getSystemName=()=>{const{Platform:e}=require("react-native");return e.OS},getBundleId=()=>{const e=(0,_index.getConfigValue)("appId");if(e)return e;const r=((0,_appBuild.getDeviceInfo)()??_appBuild.expoApplicationBackend).getBundleId();if(r)return r;throw missing("store identifier","appId")},getBuildNumber=()=>{const e=(0,_appBuild.readAppBuild)();if(null!==e)return e;throw missing("build number","appBuild")},getCountry=()=>(0,_index.getConfigValue)("country","in").toLowerCase(),subscribeMinimum=e=>(0,_minAppVersion.addMinAppVersionListener)(()=>e()),getFallbackStoreUrl=()=>{const e=getSystemName().toLowerCase(),r=getCountry();if("ios"===e){const e=(0,_index.getConfigValue)("appStoreId");return e?`https://apps.apple.com/${r}/app/id${e}`:`https://apps.apple.com/${r}/app/${getBundleId()}`}return`https://play.google.com/store/apps/details?id=${getBundleId()}&hl=${r}`},storeUrlOrNull=()=>{try{return getFallbackStoreUrl()}catch{return null}},checkForUpdates=()=>{const e=(0,_buildNumber.isBuildBelowMinimum)(getBuildNumber(),(0,_minAppVersion.getMinAppVersion)());return{isNewVersionAvailable:e,updateType:e?"major":null,url:e?storeUrlOrNull():null}},VersionCheck=exports.VersionCheck={isNewVersionAvailable:async()=>checkForUpdates(),openUpdateUrl:async e=>{const{Linking:r}=require("react-native");try{if(e)_index2.Logger.info(`Opening app store URL: ${e}`),await r.openURL(e);else{const e=getFallbackStoreUrl();_index2.Logger.info(`Opening fallback store URL: ${e}`),await r.openURL(e)}return!0}catch(t){const i=t instanceof Error?t.message:String(t);_index2.Logger.error(`Failed to open store URL: ${i}`);try{if(e){const e=getFallbackStoreUrl();return _index2.Logger.info(`Trying fallback store URL: ${e}`),await r.openURL(e),!0}}catch(e){const r=e instanceof Error?e.message:String(e);_index2.Logger.error(`Failed to open fallback URL: ${r}`)}return!1}},getFallbackStoreUrl:getFallbackStoreUrl},NOT_REQUIRED={isUpdateRequired:!1,updateUrl:null},useVersionCheck=(e,r=(0,_index.getConfigValue)("environment","Development"))=>{const t=(0,_react.useSyncExternalStore)(subscribeMinimum,_minAppVersion.getMinAppVersion,_minAppVersion.getMinAppVersion);return(0,_react.useMemo)(()=>{if(!e||"Production"!==r||null===t)return NOT_REQUIRED;try{if(!(0,_buildNumber.isBuildBelowMinimum)(getBuildNumber(),t))return NOT_REQUIRED}catch{return NOT_REQUIRED}return{isUpdateRequired:!0,updateUrl:storeUrlOrNull()}},[e,r,t])};exports.useVersionCheck=useVersionCheck;var _default=exports.default=VersionCheck;
@@ -1 +1 @@
1
- "use strict";import{getConfigValue}from"../config/index.js";export const CLIENT_BUILD_HEADER="X-Client-Build";export const CLIENT_KIND_HEADER="X-Client-Kind";export const ACCOUNT_ID_HEADER="X-Account-Id";export const CLIENT_KIND="MobileApp";export const getClientBuild=()=>{const t=getConfigValue("appVersion");if(!t)return null;const e=getConfigValue("commit");return e?`${t}+${e}`:t};const setIfAbsent=(t,e,n)=>{n&&!t.has(e)&&t.set(e,n)};export const applyClientHeaders=t=>{t.interceptors.request.use(t=>(setIfAbsent(t.headers,"X-Client-Kind","MobileApp"),setIfAbsent(t.headers,"X-Client-Build",getClientBuild()),t))};export const applyAccountIdHeader=async(t,e)=>{const n=e?await e():null;n&&t.set("X-Account-Id",n)};
1
+ "use strict";import{getConfigValue}from"../config/index.js";import{readAppBuild}from"../versionCheck/appBuild.js";export const CLIENT_BUILD_HEADER="X-Client-Build";export const APP_BUILD_HEADER="X-App-Build";export const CLIENT_KIND_HEADER="X-Client-Kind";export const ACCOUNT_ID_HEADER="X-Account-Id";export const CLIENT_KIND="MobileApp";export const getClientBuild=()=>{const e=getConfigValue("appVersion");if(!e)return null;const t=getConfigValue("commit");return t?`${e}+${t}`:e};const setIfAbsent=(e,t,n)=>{n&&!e.has(t)&&e.set(t,n)};export const applyClientHeaders=e=>{e.interceptors.request.use(e=>{setIfAbsent(e.headers,"X-Client-Kind","MobileApp"),setIfAbsent(e.headers,"X-Client-Build",getClientBuild());const t=readAppBuild();return setIfAbsent(e.headers,"X-App-Build",null===t?null:String(t)),e})};export const applyAccountIdHeader=async(e,t)=>{const n=t?await t():null;n&&e.set("X-Account-Id",n)};
@@ -1 +1 @@
1
- "use strict";import{Logger}from"../logger/index.js";export const MIN_APP_VERSION_HEADER="X-Min-App-Version";let current=null;const listeners=new Set;export const getMinAppVersion=()=>current;export const addMinAppVersionListener=e=>(listeners.add(e),null!==current&&e(current),()=>{listeners.delete(e)});const readHeader=e=>{if(!e||"object"!=typeof e)return;const r=e.get;return"function"==typeof r?r.call(e,"X-Min-App-Version"):e["X-Min-App-Version".toLowerCase()]},record=e=>{const r=readHeader(e?.headers);if(null==r||""===r)return;const n=String(r).trim();if(!/^\d+$/.test(n))return void Logger.warn(`[minAppVersion] ignored a non-integer X-Min-App-Version: ${n}`);const t=Number.parseInt(n,10);if(t!==current){current=t;for(const e of[...listeners])try{e(t)}catch(e){Logger.error("[minAppVersion] a listener threw",e)}}};export const readMinAppVersion=e=>{e.interceptors.response.use(e=>(record(e),e),e=>(record(e?.response),Promise.reject(e)))};
1
+ "use strict";import{Logger}from"../logger/index.js";import{readAppBuild}from"../versionCheck/appBuild.js";import{parseBuildNumber}from"../versionCheck/buildNumber.js";export const MIN_APP_VERSION_HEADER="X-Min-App-Version";let current=null;const listeners=new Set;export const getMinAppVersion=()=>current;export const addMinAppVersionListener=e=>(listeners.add(e),null!==current&&e(current),()=>{listeners.delete(e)});const readHeader=e=>{if(!e||"object"!=typeof e)return;const r=e.get;return"function"==typeof r?r.call(e,"X-Min-App-Version"):e["X-Min-App-Version".toLowerCase()]},setFloor=e=>{if(e!==current){current=e;for(const r of[...listeners])try{r(e)}catch(e){Logger.error("[minAppVersion] a listener threw",e)}}},record=e=>{const r=readHeader(e?.headers);if(null==r||""===r)return;const t=String(r).trim();/^\d+$/.test(t)?setFloor(Number.parseInt(t,10)):Logger.warn(`[minAppVersion] ignored a non-integer X-Min-App-Version: ${t}`)},bodyField=(e,r)=>e&&"object"==typeof e?e[r]:void 0,recordUpdateRequired=e=>{if(426!==e?.status||"client_update_required"!==bodyField(e.data,"code"))return;const r=readAppBuild(),t=parseBuildNumber(bodyField(e.data,"minVersion"))??(null===r?null:r+1);null!==t&&setFloor(Math.max(current??0,t))};export const readMinAppVersion=e=>{e.interceptors.response.use(e=>(record(e),e),e=>{const r=e?.response;return record(r),recordUpdateRequired(r),Promise.reject(e)})};
@@ -0,0 +1 @@
1
+ "use strict";import{Logger}from"../logger/index.js";import{Storage}from"../storage/index.js";const TAG="[reviewPrompt]",DAY_MS=864e5;export const DEFAULT_REVIEW_COOL_DOWN_DAYS=90;export const REVIEW_PROMPT_STORAGE_KEY="@webority/review-prompt";const storeReview=()=>require("expo-store-review");let coolDownDays=90,inFlight=!1;export const configureReviewPrompt=(e={})=>{const o=e.coolDownDays??90;if(!Number.isFinite(o)||o<0)throw new Error(`[@webority/mobile-core] configureReviewPrompt: coolDownDays must be a number of days, 0 or more; got ${String(o)}.`);o<90&&Logger.warn(`${TAG} coolDownDays ${o} is under the 90-day minimum for store builds.`),coolDownDays=o};const withinCoolDown=(e,o)=>{if(!e)return!1;const t=Date.parse(e.lastAskedUtc);return!Number.isNaN(t)&&o-t<coolDownDays*DAY_MS},storeCanPrompt=async()=>{try{const e=storeReview();return await e.isAvailableAsync()&&await e.hasAction()}catch(e){return Logger.warn(`${TAG} the store review module is not available`,e),!1}};export const requestReviewAfterSuccess=async e=>{if(inFlight)return"skipped-cooldown";inFlight=!0;try{const o=await Storage.getJson("@webority/review-prompt"),t=Date.now();if(withinCoolDown(o,t))return"skipped-cooldown";if(!await storeCanPrompt())return"skipped-unavailable";try{await storeReview().requestReview()}catch(o){return Logger.warn(`${TAG} the store review request failed after "${e}"`,o),"skipped-unavailable"}const r={lastAskedUtc:new Date(t).toISOString(),moment:e};return await Storage.setJson("@webority/review-prompt",r)||Logger.warn(`${TAG} the prompt time could not be stored, so the cool-down may not hold`),Logger.info(`${TAG} review prompt requested after "${e}"`),"shown"}finally{inFlight=!1}};
@@ -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});
@@ -0,0 +1 @@
1
+ "use strict";import*as Application from"expo-application";import{getConfigValue}from"../config/index.js";import{parseBuildNumber}from"./buildNumber.js";export const expoApplicationBackend={getBundleId:()=>Application.applicationId??"",getVersion:()=>Application.nativeApplicationVersion??"",getBuildNumber:()=>Application.nativeBuildVersion??""};let deviceInfo=null;export const setDeviceInfo=e=>{deviceInfo=e};export const getDeviceInfo=()=>deviceInfo;export const readAppBuild=()=>{const e=parseBuildNumber(getConfigValue("appBuild"));if(null!==e)return e;try{return parseBuildNumber(deviceInfo?.getBuildNumber?.()??expoApplicationBackend.getBuildNumber())}catch{return null}};
@@ -1 +1 @@
1
- "use strict";import*as Application from"expo-application";import{useMemo,useSyncExternalStore}from"react";import{addMinAppVersionListener,getMinAppVersion}from"../api/minAppVersion.js";import{getConfigValue}from"../config/index.js";import{Logger}from"../logger/index.js";import{isBuildBelowMinimum,parseBuildNumber}from"./buildNumber.js";const expoApplicationBackend={getBundleId:()=>Application.applicationId??"",getVersion:()=>Application.nativeApplicationVersion??"",getBuildNumber:()=>Application.nativeBuildVersion??""};let deviceInfo=null;export const setDeviceInfoImplementation=e=>{deviceInfo=e};const missing=(e,r)=>{const t=new Error(`[VersionCheck] the app's ${e} is not available. Call setConfig({ ${r}: ... }) — every Webority app already has this value in appsettings.Compiled.json — or setDeviceInfoImplementation() with a native provider.`);return Logger.error(t.message),t},getSystemName=()=>{const{Platform:e}=require("react-native");return e.OS},getBundleId=()=>{const e=getConfigValue("appId");if(e)return e;const r=(deviceInfo??expoApplicationBackend).getBundleId();if(r)return r;throw missing("store identifier","appId")},getBuildNumber=()=>{const e=parseBuildNumber(getConfigValue("appBuild"));if(null!==e)return e;const r=parseBuildNumber(deviceInfo?.getBuildNumber?.()??expoApplicationBackend.getBuildNumber());if(null!==r)return r;throw missing("build number","appBuild")},getCountry=()=>getConfigValue("country","in").toLowerCase(),subscribeMinimum=e=>addMinAppVersionListener(()=>e()),getFallbackStoreUrl=()=>{const e=getSystemName().toLowerCase(),r=getCountry();if("ios"===e){const e=getConfigValue("appStoreId");return e?`https://apps.apple.com/${r}/app/id${e}`:`https://apps.apple.com/${r}/app/${getBundleId()}`}return`https://play.google.com/store/apps/details?id=${getBundleId()}&hl=${r}`},storeUrlOrNull=()=>{try{return getFallbackStoreUrl()}catch{return null}},checkForUpdates=()=>{const e=isBuildBelowMinimum(getBuildNumber(),getMinAppVersion());return{isNewVersionAvailable:e,updateType:e?"major":null,url:e?storeUrlOrNull():null}};export const VersionCheck={isNewVersionAvailable:async()=>checkForUpdates(),openUpdateUrl:async e=>{const{Linking:r}=require("react-native");try{if(e)Logger.info(`Opening app store URL: ${e}`),await r.openURL(e);else{const e=getFallbackStoreUrl();Logger.info(`Opening fallback store URL: ${e}`),await r.openURL(e)}return!0}catch(t){const i=t instanceof Error?t.message:String(t);Logger.error(`Failed to open store URL: ${i}`);try{if(e){const e=getFallbackStoreUrl();return Logger.info(`Trying fallback store URL: ${e}`),await r.openURL(e),!0}}catch(e){const r=e instanceof Error?e.message:String(e);Logger.error(`Failed to open fallback URL: ${r}`)}return!1}},getFallbackStoreUrl:getFallbackStoreUrl};const NOT_REQUIRED={isUpdateRequired:!1,updateUrl:null};export const useVersionCheck=(e,r=getConfigValue("environment","Development"))=>{const t=useSyncExternalStore(subscribeMinimum,getMinAppVersion,getMinAppVersion);return useMemo(()=>{if(!e||"Production"!==r||null===t)return NOT_REQUIRED;try{if(!isBuildBelowMinimum(getBuildNumber(),t))return NOT_REQUIRED}catch{return NOT_REQUIRED}return{isUpdateRequired:!0,updateUrl:storeUrlOrNull()}},[e,r,t])};export default VersionCheck;export{isBuildBelowMinimum,parseBuildNumber}from"./buildNumber.js";
1
+ "use strict";import{useMemo,useSyncExternalStore}from"react";import{addMinAppVersionListener,getMinAppVersion}from"../api/minAppVersion.js";import{getConfigValue}from"../config/index.js";import{Logger}from"../logger/index.js";import{expoApplicationBackend,getDeviceInfo,readAppBuild,setDeviceInfo}from"./appBuild.js";import{isBuildBelowMinimum}from"./buildNumber.js";export const setDeviceInfoImplementation=e=>{setDeviceInfo(e)};const missing=(e,r)=>{const t=new Error(`[VersionCheck] the app's ${e} is not available. Call setConfig({ ${r}: ... }) — every Webority app already has this value in appsettings.Compiled.json — or setDeviceInfoImplementation() with a native provider.`);return Logger.error(t.message),t},getSystemName=()=>{const{Platform:e}=require("react-native");return e.OS},getBundleId=()=>{const e=getConfigValue("appId");if(e)return e;const r=(getDeviceInfo()??expoApplicationBackend).getBundleId();if(r)return r;throw missing("store identifier","appId")},getBuildNumber=()=>{const e=readAppBuild();if(null!==e)return e;throw missing("build number","appBuild")},getCountry=()=>getConfigValue("country","in").toLowerCase(),subscribeMinimum=e=>addMinAppVersionListener(()=>e()),getFallbackStoreUrl=()=>{const e=getSystemName().toLowerCase(),r=getCountry();if("ios"===e){const e=getConfigValue("appStoreId");return e?`https://apps.apple.com/${r}/app/id${e}`:`https://apps.apple.com/${r}/app/${getBundleId()}`}return`https://play.google.com/store/apps/details?id=${getBundleId()}&hl=${r}`},storeUrlOrNull=()=>{try{return getFallbackStoreUrl()}catch{return null}},checkForUpdates=()=>{const e=isBuildBelowMinimum(getBuildNumber(),getMinAppVersion());return{isNewVersionAvailable:e,updateType:e?"major":null,url:e?storeUrlOrNull():null}};export const VersionCheck={isNewVersionAvailable:async()=>checkForUpdates(),openUpdateUrl:async e=>{const{Linking:r}=require("react-native");try{if(e)Logger.info(`Opening app store URL: ${e}`),await r.openURL(e);else{const e=getFallbackStoreUrl();Logger.info(`Opening fallback store URL: ${e}`),await r.openURL(e)}return!0}catch(t){const o=t instanceof Error?t.message:String(t);Logger.error(`Failed to open store URL: ${o}`);try{if(e){const e=getFallbackStoreUrl();return Logger.info(`Trying fallback store URL: ${e}`),await r.openURL(e),!0}}catch(e){const r=e instanceof Error?e.message:String(e);Logger.error(`Failed to open fallback URL: ${r}`)}return!1}},getFallbackStoreUrl:getFallbackStoreUrl};const NOT_REQUIRED={isUpdateRequired:!1,updateUrl:null};export const useVersionCheck=(e,r=getConfigValue("environment","Development"))=>{const t=useSyncExternalStore(subscribeMinimum,getMinAppVersion,getMinAppVersion);return useMemo(()=>{if(!e||"Production"!==r||null===t)return NOT_REQUIRED;try{if(!isBuildBelowMinimum(getBuildNumber(),t))return NOT_REQUIRED}catch{return NOT_REQUIRED}return{isUpdateRequired:!0,updateUrl:storeUrlOrNull()}},[e,r,t])};export default VersionCheck;export{readAppBuild}from"./appBuild.js";export{isBuildBelowMinimum,parseBuildNumber}from"./buildNumber.js";
@@ -1,5 +1,6 @@
1
1
  import type { AxiosInstance } from 'axios';
2
2
  export declare const CLIENT_BUILD_HEADER = "X-Client-Build";
3
+ export declare const APP_BUILD_HEADER = "X-App-Build";
3
4
  export declare const CLIENT_KIND_HEADER = "X-Client-Kind";
4
5
  export declare const ACCOUNT_ID_HEADER = "X-Account-Id";
5
6
  /** The server picks session rules and the minimum version per kind; the names match its enum. */
@@ -8,7 +9,10 @@ export declare const CLIENT_KIND: ClientKind;
8
9
  /** `<appVersion>+<commit>`, `<appVersion>` when no commit was stamped, `null` before init. */
9
10
  export declare const getClientBuild: () => string | null;
10
11
  /**
11
- * Stamps X-Client-Kind and X-Client-Build on every request. A value the app
12
+ * Stamps X-Client-Kind, X-Client-Build and X-App-Build on every request.
13
+ * X-App-Build is the whole build number the server compares with its minimum;
14
+ * it is left off when the build is unknown, because the server never refuses a
15
+ * request without it and a guessed number could strand a current build. A value the app
12
16
  * already set (a default header, or a per-request one) wins, so an app that
13
17
  * wired them by hand keeps working unchanged.
14
18
  */
@@ -11,7 +11,9 @@
11
11
  * @webority/mobile-core/sqlite expo-sqlite
12
12
  * @webority/mobile-core/download a filesystem module
13
13
  * @webority/mobile-core/share expo-sharing
14
+ * @webority/mobile-core/review expo-store-review
14
15
  * @webority/mobile-core/secureScreen expo-screen-capture
16
+ * @webority/mobile-core/updates expo-updates
15
17
  * @webority/mobile-core/smsRetriever Android only, own TurboModule
16
18
  * @webority/mobile-core/query RTK Query base query and createApi preset
17
19
  * (@reduxjs/toolkit and react-redux)
@@ -0,0 +1,42 @@
1
+ /** The fleet's minimum: no more than one prompt in 90 days per user. */
2
+ export declare const DEFAULT_REVIEW_COOL_DOWN_DAYS = 90;
3
+ /** Where the last prompt is remembered, through mobile-core's `Storage`. */
4
+ export declare const REVIEW_PROMPT_STORAGE_KEY = "@webority/review-prompt";
5
+ export interface ReviewPromptOptions {
6
+ /** Days to wait after a prompt before the next one. Defaults to 90, the fleet minimum. */
7
+ coolDownDays?: number;
8
+ }
9
+ /** What was stored when the prompt last showed. */
10
+ export interface ReviewPromptRecord {
11
+ /** When the prompt was last requested, as an ISO 8601 UTC string. */
12
+ lastAskedUtc: string;
13
+ /** The success moment that asked. */
14
+ moment: string;
15
+ }
16
+ export type ReviewPromptResult = 'shown' | 'skipped-cooldown' | 'skipped-unavailable';
17
+ /**
18
+ * Sets the cool-down between prompts. Each call replaces the previous options, so
19
+ * `configureReviewPrompt({})` restores the default. Throws on a negative or
20
+ * non-finite number of days, which is a programming error.
21
+ */
22
+ export declare const configureReviewPrompt: (options?: ReviewPromptOptions) => void;
23
+ /**
24
+ * Shows the platform's rating prompt after a success moment, at most once per
25
+ * cool-down (90 days by default, see `configureReviewPrompt`).
26
+ *
27
+ * Call it only after a success moment and once for that moment; never after an
28
+ * error and never behind a "Do you like the app?" question (App Store guideline
29
+ * 5.6.1, Google Play's review policy).
30
+ *
31
+ * Resolves, never rejects:
32
+ * - `shown`: the prompt was requested and the time and `moment` were stored. The
33
+ * platform still decides whether it appears, and caps how often it does.
34
+ * - `skipped-cooldown`: the last prompt is inside the cool-down, or another call
35
+ * is still running.
36
+ * - `skipped-unavailable`: the device or build has no in-app review (no store,
37
+ * a simulator, the native module missing), or the request failed. Logged.
38
+ *
39
+ * @param moment a short name for the success moment, such as `survey-submitted`.
40
+ */
41
+ export declare const requestReviewAfterSuccess: (moment: string) => Promise<ReviewPromptResult>;
42
+ //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Minimal device-info shape we depend on.
3
+ *
4
+ * This is a LAST RESORT, not the primary path. Every Webority app carries its
5
+ * store id and build number in `appsettings.Compiled.json`, so
6
+ * `setConfig({ appId, appBuild })` covers both without a native module, and
7
+ * expo-application covers an app that wires neither. `getBuildNumber` is
8
+ * optional so an injector written before the server-driven check still fits;
9
+ * without it the build number comes from expo-application.
10
+ */
11
+ export interface DeviceInfoLike {
12
+ getBundleId: () => string;
13
+ getVersion: () => string;
14
+ getBuildNumber?: () => string;
15
+ }
16
+ /**
17
+ * expo-application, read at call time: its values are module CONSTANTS, and
18
+ * capturing them here would snapshot them before the native module has
19
+ * initialised. Config still wins over this.
20
+ */
21
+ export declare const expoApplicationBackend: Required<DeviceInfoLike>;
22
+ export declare const setDeviceInfo: (impl: DeviceInfoLike | null) => void;
23
+ export declare const getDeviceInfo: () => DeviceInfoLike | null;
24
+ /**
25
+ * The app's internal build number (`VERSION_INTERNAL`: Android versionCode, iOS
26
+ * CFBundleVersion) from `setConfig({ appBuild })`, else the device info, else
27
+ * expo-application; `null` when none of them holds an integer. Never throws.
28
+ */
29
+ export declare const readAppBuild: () => number | null;
30
+ //# sourceMappingURL=appBuild.d.ts.map
@@ -1,19 +1,5 @@
1
1
  import { type Environment } from '../config';
2
- /**
3
- * Minimal device-info shape we depend on.
4
- *
5
- * This is a LAST RESORT, not the primary path. Every Webority app carries its
6
- * store id and build number in `appsettings.Compiled.json`, so
7
- * `setConfig({ appId, appBuild })` covers both without a native module, and
8
- * expo-application covers an app that wires neither. `getBuildNumber` is
9
- * optional so an injector written before the server-driven check still fits;
10
- * without it the build number comes from expo-application.
11
- */
12
- interface DeviceInfoLike {
13
- getBundleId: () => string;
14
- getVersion: () => string;
15
- getBuildNumber?: () => string;
16
- }
2
+ import { type DeviceInfoLike } from './appBuild';
17
3
  /**
18
4
  * Supply the app identity from a native module instead of config.
19
5
  * Prefer `setConfig({ appId, appBuild })` — it needs no native dependency.
@@ -86,6 +72,7 @@ export declare const VersionCheck: VersionCheckType;
86
72
  */
87
73
  export declare const useVersionCheck: (isAppReady: boolean, environment?: Environment) => UpdateInfo;
88
74
  export default VersionCheck;
75
+ export { readAppBuild } from './appBuild';
89
76
  export { isBuildBelowMinimum, parseBuildNumber } from './buildNumber';
90
77
  export type { DeviceInfoLike };
91
78
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import type { AxiosInstance } from 'axios';
2
2
  export declare const CLIENT_BUILD_HEADER = "X-Client-Build";
3
+ export declare const APP_BUILD_HEADER = "X-App-Build";
3
4
  export declare const CLIENT_KIND_HEADER = "X-Client-Kind";
4
5
  export declare const ACCOUNT_ID_HEADER = "X-Account-Id";
5
6
  /** The server picks session rules and the minimum version per kind; the names match its enum. */
@@ -8,7 +9,10 @@ export declare const CLIENT_KIND: ClientKind;
8
9
  /** `<appVersion>+<commit>`, `<appVersion>` when no commit was stamped, `null` before init. */
9
10
  export declare const getClientBuild: () => string | null;
10
11
  /**
11
- * Stamps X-Client-Kind and X-Client-Build on every request. A value the app
12
+ * Stamps X-Client-Kind, X-Client-Build and X-App-Build on every request.
13
+ * X-App-Build is the whole build number the server compares with its minimum;
14
+ * it is left off when the build is unknown, because the server never refuses a
15
+ * request without it and a guessed number could strand a current build. A value the app
12
16
  * already set (a default header, or a per-request one) wins, so an app that
13
17
  * wired them by hand keeps working unchanged.
14
18
  */
@@ -11,7 +11,9 @@
11
11
  * @webority/mobile-core/sqlite expo-sqlite
12
12
  * @webority/mobile-core/download a filesystem module
13
13
  * @webority/mobile-core/share expo-sharing
14
+ * @webority/mobile-core/review expo-store-review
14
15
  * @webority/mobile-core/secureScreen expo-screen-capture
16
+ * @webority/mobile-core/updates expo-updates
15
17
  * @webority/mobile-core/smsRetriever Android only, own TurboModule
16
18
  * @webority/mobile-core/query RTK Query base query and createApi preset
17
19
  * (@reduxjs/toolkit and react-redux)
@@ -0,0 +1,42 @@
1
+ /** The fleet's minimum: no more than one prompt in 90 days per user. */
2
+ export declare const DEFAULT_REVIEW_COOL_DOWN_DAYS = 90;
3
+ /** Where the last prompt is remembered, through mobile-core's `Storage`. */
4
+ export declare const REVIEW_PROMPT_STORAGE_KEY = "@webority/review-prompt";
5
+ export interface ReviewPromptOptions {
6
+ /** Days to wait after a prompt before the next one. Defaults to 90, the fleet minimum. */
7
+ coolDownDays?: number;
8
+ }
9
+ /** What was stored when the prompt last showed. */
10
+ export interface ReviewPromptRecord {
11
+ /** When the prompt was last requested, as an ISO 8601 UTC string. */
12
+ lastAskedUtc: string;
13
+ /** The success moment that asked. */
14
+ moment: string;
15
+ }
16
+ export type ReviewPromptResult = 'shown' | 'skipped-cooldown' | 'skipped-unavailable';
17
+ /**
18
+ * Sets the cool-down between prompts. Each call replaces the previous options, so
19
+ * `configureReviewPrompt({})` restores the default. Throws on a negative or
20
+ * non-finite number of days, which is a programming error.
21
+ */
22
+ export declare const configureReviewPrompt: (options?: ReviewPromptOptions) => void;
23
+ /**
24
+ * Shows the platform's rating prompt after a success moment, at most once per
25
+ * cool-down (90 days by default, see `configureReviewPrompt`).
26
+ *
27
+ * Call it only after a success moment and once for that moment; never after an
28
+ * error and never behind a "Do you like the app?" question (App Store guideline
29
+ * 5.6.1, Google Play's review policy).
30
+ *
31
+ * Resolves, never rejects:
32
+ * - `shown`: the prompt was requested and the time and `moment` were stored. The
33
+ * platform still decides whether it appears, and caps how often it does.
34
+ * - `skipped-cooldown`: the last prompt is inside the cool-down, or another call
35
+ * is still running.
36
+ * - `skipped-unavailable`: the device or build has no in-app review (no store,
37
+ * a simulator, the native module missing), or the request failed. Logged.
38
+ *
39
+ * @param moment a short name for the success moment, such as `survey-submitted`.
40
+ */
41
+ export declare const requestReviewAfterSuccess: (moment: string) => Promise<ReviewPromptResult>;
42
+ //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Minimal device-info shape we depend on.
3
+ *
4
+ * This is a LAST RESORT, not the primary path. Every Webority app carries its
5
+ * store id and build number in `appsettings.Compiled.json`, so
6
+ * `setConfig({ appId, appBuild })` covers both without a native module, and
7
+ * expo-application covers an app that wires neither. `getBuildNumber` is
8
+ * optional so an injector written before the server-driven check still fits;
9
+ * without it the build number comes from expo-application.
10
+ */
11
+ export interface DeviceInfoLike {
12
+ getBundleId: () => string;
13
+ getVersion: () => string;
14
+ getBuildNumber?: () => string;
15
+ }
16
+ /**
17
+ * expo-application, read at call time: its values are module CONSTANTS, and
18
+ * capturing them here would snapshot them before the native module has
19
+ * initialised. Config still wins over this.
20
+ */
21
+ export declare const expoApplicationBackend: Required<DeviceInfoLike>;
22
+ export declare const setDeviceInfo: (impl: DeviceInfoLike | null) => void;
23
+ export declare const getDeviceInfo: () => DeviceInfoLike | null;
24
+ /**
25
+ * The app's internal build number (`VERSION_INTERNAL`: Android versionCode, iOS
26
+ * CFBundleVersion) from `setConfig({ appBuild })`, else the device info, else
27
+ * expo-application; `null` when none of them holds an integer. Never throws.
28
+ */
29
+ export declare const readAppBuild: () => number | null;
30
+ //# sourceMappingURL=appBuild.d.ts.map
@@ -1,19 +1,5 @@
1
1
  import { type Environment } from '../config/index.js';
2
- /**
3
- * Minimal device-info shape we depend on.
4
- *
5
- * This is a LAST RESORT, not the primary path. Every Webority app carries its
6
- * store id and build number in `appsettings.Compiled.json`, so
7
- * `setConfig({ appId, appBuild })` covers both without a native module, and
8
- * expo-application covers an app that wires neither. `getBuildNumber` is
9
- * optional so an injector written before the server-driven check still fits;
10
- * without it the build number comes from expo-application.
11
- */
12
- interface DeviceInfoLike {
13
- getBundleId: () => string;
14
- getVersion: () => string;
15
- getBuildNumber?: () => string;
16
- }
2
+ import { type DeviceInfoLike } from './appBuild.js';
17
3
  /**
18
4
  * Supply the app identity from a native module instead of config.
19
5
  * Prefer `setConfig({ appId, appBuild })` — it needs no native dependency.
@@ -86,6 +72,7 @@ export declare const VersionCheck: VersionCheckType;
86
72
  */
87
73
  export declare const useVersionCheck: (isAppReady: boolean, environment?: Environment) => UpdateInfo;
88
74
  export default VersionCheck;
75
+ export { readAppBuild } from './appBuild.js';
89
76
  export { isBuildBelowMinimum, parseBuildNumber } from './buildNumber.js';
90
77
  export type { DeviceInfoLike };
91
78
  //# sourceMappingURL=index.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.6.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",
@@ -161,6 +162,16 @@
161
162
  "default": "./lib/commonjs/share/index.js"
162
163
  }
163
164
  },
165
+ "./review": {
166
+ "import": {
167
+ "types": "./lib/typescript/module/review/index.d.ts",
168
+ "default": "./lib/module/review/index.js"
169
+ },
170
+ "require": {
171
+ "types": "./lib/typescript/commonjs/review/index.d.ts",
172
+ "default": "./lib/commonjs/review/index.js"
173
+ }
174
+ },
164
175
  "./secureScreen": {
165
176
  "import": {
166
177
  "types": "./lib/typescript/module/secureScreen/index.d.ts",
@@ -231,6 +242,16 @@
231
242
  "default": "./lib/commonjs/redux/index.js"
232
243
  }
233
244
  },
245
+ "./updates": {
246
+ "import": {
247
+ "types": "./lib/typescript/module/updates/index.d.ts",
248
+ "default": "./lib/module/updates/index.js"
249
+ },
250
+ "require": {
251
+ "types": "./lib/typescript/commonjs/updates/index.d.ts",
252
+ "default": "./lib/commonjs/updates/index.js"
253
+ }
254
+ },
234
255
  "./package.json": "./package.json"
235
256
  },
236
257
  "main": "./lib/commonjs/index.js",
@@ -258,6 +279,7 @@
258
279
  }
259
280
  },
260
281
  "scripts": {
282
+ "prepack": "node ../../scripts/package-guard-pack.mjs --output-dirs lib --allow-file ../../.webority/package-allow.json",
261
283
  "build": "bob build && node ../../scripts/minify-lib.mjs",
262
284
  "clean": "rimraf lib",
263
285
  "lint": "biome lint ./src",
@@ -287,7 +309,9 @@
287
309
  "expo-secure-store": ">=57.0.0",
288
310
  "expo-sharing": ">=57.0.0",
289
311
  "expo-sqlite": ">=57.0.0",
312
+ "expo-store-review": ">=57.0.0",
290
313
  "expo-task-manager": ">=57.0.0",
314
+ "expo-updates": ">=57.0.0",
291
315
  "react": "^19.1.0",
292
316
  "react-native": ">=0.81.0",
293
317
  "react-redux": "^9.3.0"
@@ -329,9 +353,15 @@
329
353
  "expo-sharing": {
330
354
  "optional": true
331
355
  },
356
+ "expo-store-review": {
357
+ "optional": true
358
+ },
332
359
  "expo-task-manager": {
333
360
  "optional": true
334
361
  },
362
+ "expo-updates": {
363
+ "optional": true
364
+ },
335
365
  "react-redux": {
336
366
  "optional": true
337
367
  }