@webority/mobile-core 0.112.3 → 0.114.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
@@ -208,6 +208,9 @@ and answers an older build with 426 `client_update_required`. The number comes f
208
208
  `nativeBuildVersion`. Without a whole number the header is left off, and the server never refuses a
209
209
  request without it. `Client-Build` (`<appVersion>+<commit>`) is unchanged.
210
210
 
211
+ Both HTTP clients also send `Client-App: <appId>` when the config has an app id (`initWebority({ appId })`).
212
+ It is the app's store identifier, so the API can tell apps of the same client kind apart.
213
+
211
214
  The version floor comes only from the `Min-App-Version` response header. When a reply carries it,
212
215
  `useVersionCheck` turns `isUpdateRequired` on for a build below it. A 426 `client_update_required`
213
216
  does not move the floor, so a 426 without that header leaves the floor where it is. A 426 is never
@@ -233,6 +236,20 @@ JWT-shaped strings, and the values of keys named `token`, `accessToken`, `refres
233
236
 
234
237
  `configureLogger({ redactKeys })` adds app keys to the built-in masked list and never removes one. `Logger.getBreadcrumbs()` returns the last 50 breadcrumbs, oldest first, already redacted; `configureLogger({ breadcrumbLimit })` changes the cap. `initWebority` takes a required `remoteLogger`: the sink, or `null` for a build without crash reporting. It also takes a required `appId`, the compiled settings' app id, and refuses to start when it differs from the installed app's id: a bundle prebuilt for another environment stops at start-up with the prebuild step to run.
235
238
 
239
+ ## App lock
240
+
241
+ `@webority/mobile-core/appLock` locks the app when a signed-in person returns after the lock delay (five minutes by default). `useAppLock` returns what a settings screen and the lock screen need:
242
+
243
+ ```ts
244
+ import { useAppLock } from '@webority/mobile-core/appLock';
245
+
246
+ const { locked, enabled, biometricAvailable, availabilityKnown, setEnabled, unlock } = useAppLock({ active: isSignedIn });
247
+ ```
248
+
249
+ - `availabilityKnown` is false until the first biometric check settles. Until then `biometricAvailable` is not an answer yet, so a screen should wait before it shows the lock as unavailable or disables its switch.
250
+ - `setEnabled(next)` and `unlock(reason)` return an `AppLockResult`: `{ ok: true }`, or `{ ok: false, reason }` with the reason `cancelled`, `lockout`, `not-enrolled`, `not-available` or `failed`.
251
+ - `policy` overrides the delay and the two storage keys; the defaults are used for any key left out.
252
+
236
253
  ## Metro bridge
237
254
 
238
255
  `@webority/mobile-core/metro` builds the development bridge that a Metro dev tool reads as
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.stampAccountId=exports.readAccountId=exports.getClientBuild=exports.applyClientHeaders=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="Client-Build",APP_BUILD_HEADER=exports.APP_BUILD_HEADER="App-Build",CLIENT_KIND_HEADER=exports.CLIENT_KIND_HEADER="Client-Kind",ACCOUNT_ID_HEADER=exports.ACCOUNT_ID_HEADER="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 readAccountId=async e=>e&&await e()||null;exports.readAccountId=readAccountId;const stampAccountId=async(e,t)=>{if(void 0!==e._accountId)return void(e._accountId&&e.headers.set(ACCOUNT_ID_HEADER,e._accountId));const s=e.headers.get(ACCOUNT_ID_HEADER);if("string"==typeof s&&""!==s)return void(e._accountId=s);const n=await readAccountId(t);e._accountId=n,n&&e.headers.set(ACCOUNT_ID_HEADER,n)};exports.stampAccountId=stampAccountId;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.stampAccountId=exports.readAccountId=exports.getClientBuild=exports.applyClientHeaders=exports.CLIENT_KIND_HEADER=exports.CLIENT_KIND=exports.CLIENT_BUILD_HEADER=exports.CLIENT_APP_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="Client-Build",APP_BUILD_HEADER=exports.APP_BUILD_HEADER="App-Build",CLIENT_KIND_HEADER=exports.CLIENT_KIND_HEADER="Client-Kind",CLIENT_APP_HEADER=exports.CLIENT_APP_HEADER="Client-App",ACCOUNT_ID_HEADER=exports.ACCOUNT_ID_HEADER="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_APP_HEADER,(0,_index.getConfigValue)("appId")),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 readAccountId=async e=>e&&await e()||null;exports.readAccountId=readAccountId;const stampAccountId=async(e,t)=>{if(void 0!==e._accountId)return void(e._accountId&&e.headers.set(ACCOUNT_ID_HEADER,e._accountId));const s=e.headers.get(ACCOUNT_ID_HEADER);if("string"==typeof s&&""!==s)return void(e._accountId=s);const n=await readAccountId(t);e._accountId=n,n&&e.headers.set(ACCOUNT_ID_HEADER,n)};exports.stampAccountId=stampAccountId;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.authenticateBiometric=authenticateBiometric,exports.isBiometricAvailable=isBiometricAvailable,exports.useAppLock=useAppLock;var cache,LocalAuthentication=_interopRequireWildcard(require("expo-local-authentication")),_react=require("react"),_reactNative=require("react-native"),_index=require("../logger/index.js"),_index2=require("../storage/index.js");function _interopRequireWildcard(e,t){if(!t&&e&&e.__esModule)return e;var a,r,c=Object.defineProperty,n={__proto__:null,default:e};if(Object(e)!==e)return n;if(cache||"function"!=typeof WeakMap||(cache=new WeakMap),cache){if(cache.has(e))return cache.get(e);cache.set(e,n)}for(r in e)"default"!==r&&{}.hasOwnProperty.call(e,r)&&((a=c&&Object.getOwnPropertyDescriptor(e,r))&&(a.get||a.set)?c(n,r,a):n[r]=e[r]);return n}const DEFAULT_POLICY={lockAfterMs:3e5,enabledKey:"appLockEnabled",backgroundedAtKey:"appLockBackgroundedAt"};let pendingPrompt=null;async function isBiometricAvailable(){try{const[e,t]=await Promise.all([LocalAuthentication.hasHardwareAsync(),LocalAuthentication.isEnrolledAsync()]);return e&&t}catch(e){return _index.Logger.error("[AppLock] biometric availability check failed:",e),!1}}const failureFor=e=>{switch(e){case"user_cancel":case"app_cancel":case"system_cancel":case"user_fallback":return"cancelled";case"lockout":return"lockout";case"not_enrolled":case"passcode_not_set":return"not-enrolled";case"not_available":return"not-available";default:return"failed"}},prompt=async e=>{try{if(await LocalAuthentication.getEnrolledLevelAsync()===LocalAuthentication.SecurityLevel.NONE)return{ok:!1,reason:"not-enrolled"};const t=await LocalAuthentication.authenticateAsync({promptMessage:e,cancelLabel:"Cancel",fallbackLabel:"Use passcode",disableDeviceFallback:!1});return t.success?{ok:!0}:(_index.Logger.warn("[AppLock] check not passed:",t.error),{ok:!1,reason:failureFor(t.error)})}catch(e){return _index.Logger.error("[AppLock] check failed:",e),{ok:!1,reason:"failed"}}};function authenticateBiometric(e){return pendingPrompt||(pendingPrompt=prompt(e).finally(()=>{pendingPrompt=null})),pendingPrompt}function useAppLock({active:e,policy:t}){const a=t?.lockAfterMs??DEFAULT_POLICY.lockAfterMs,r=t?.enabledKey??DEFAULT_POLICY.enabledKey,c=t?.backgroundedAtKey??DEFAULT_POLICY.backgroundedAtKey,[n,o]=(0,_react.useState)(!1),[i,l]=(0,_react.useState)(!1),[s,u]=(0,_react.useState)(!1),d=(0,_react.useCallback)(async()=>{const e=await _index2.Storage.getItem(r);return"true"===e||"false"===e?"true"===e:isBiometricAvailable()},[r]),p=(0,_react.useCallback)(async()=>{const t=await _index2.Storage.getItem(c);null!==t&&(await _index2.Storage.removeItem(c),e&&Date.now()-Number(t)>=a&&await d()&&o(!0))},[e,c,a,d]);(0,_react.useEffect)(()=>{e||o(!1),p()},[e,p]),(0,_react.useEffect)(()=>{const t=_reactNative.AppState.addEventListener("change",t=>{"background"===t&&e?_index2.Storage.setItem(c,String(Date.now())):"active"===t&&p()});return()=>t.remove()},[e,c,p]),(0,_react.useEffect)(()=>{let e=!1;return Promise.all([d(),isBiometricAvailable()]).then(([t,a])=>{e||(l(t),u(a))}),()=>{e=!0}},[d]);return{locked:n,enabled:i,biometricAvailable:s,setEnabled:(0,_react.useCallback)(async e=>await _index2.Storage.setItem(r,e?"true":"false")?(l(e),{ok:!0}):{ok:!1,reason:"failed"},[r]),unlock:(0,_react.useCallback)(async e=>{const t=await authenticateBiometric(e);return t.ok&&o(!1),t},[])}}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.authenticateBiometric=authenticateBiometric,exports.isBiometricAvailable=isBiometricAvailable,exports.useAppLock=useAppLock;var cache,LocalAuthentication=_interopRequireWildcard(require("expo-local-authentication")),_react=require("react"),_reactNative=require("react-native"),_index=require("../logger/index.js"),_index2=require("../storage/index.js");function _interopRequireWildcard(e,t){if(!t&&e&&e.__esModule)return e;var a,r,c=Object.defineProperty,n={__proto__:null,default:e};if(Object(e)!==e)return n;if(cache||"function"!=typeof WeakMap||(cache=new WeakMap),cache){if(cache.has(e))return cache.get(e);cache.set(e,n)}for(r in e)"default"!==r&&{}.hasOwnProperty.call(e,r)&&((a=c&&Object.getOwnPropertyDescriptor(e,r))&&(a.get||a.set)?c(n,r,a):n[r]=e[r]);return n}const DEFAULT_POLICY={lockAfterMs:3e5,enabledKey:"appLockEnabled",backgroundedAtKey:"appLockBackgroundedAt"};let pendingPrompt=null;async function isBiometricAvailable(){try{const[e,t]=await Promise.all([LocalAuthentication.hasHardwareAsync(),LocalAuthentication.isEnrolledAsync()]);return e&&t}catch(e){return _index.Logger.error("[AppLock] biometric availability check failed:",e),!1}}const failureFor=e=>{switch(e){case"user_cancel":case"app_cancel":case"system_cancel":case"user_fallback":return"cancelled";case"lockout":return"lockout";case"not_enrolled":case"passcode_not_set":return"not-enrolled";case"not_available":return"not-available";default:return"failed"}},prompt=async e=>{try{if(await LocalAuthentication.getEnrolledLevelAsync()===LocalAuthentication.SecurityLevel.NONE)return{ok:!1,reason:"not-enrolled"};const t=await LocalAuthentication.authenticateAsync({promptMessage:e,cancelLabel:"Cancel",fallbackLabel:"Use passcode",disableDeviceFallback:!1});return t.success?{ok:!0}:(_index.Logger.warn("[AppLock] check not passed:",t.error),{ok:!1,reason:failureFor(t.error)})}catch(e){return _index.Logger.error("[AppLock] check failed:",e),{ok:!1,reason:"failed"}}};function authenticateBiometric(e){return pendingPrompt||(pendingPrompt=prompt(e).finally(()=>{pendingPrompt=null})),pendingPrompt}function useAppLock({active:e,policy:t}){const a=t?.lockAfterMs??DEFAULT_POLICY.lockAfterMs,r=t?.enabledKey??DEFAULT_POLICY.enabledKey,c=t?.backgroundedAtKey??DEFAULT_POLICY.backgroundedAtKey,[n,o]=(0,_react.useState)(!1),[i,l]=(0,_react.useState)(!1),[s,u]=(0,_react.useState)(!1),[d,p]=(0,_react.useState)(!1),_=(0,_react.useCallback)(async()=>{const e=await _index2.Storage.getItem(r);return"true"===e||"false"===e?"true"===e:isBiometricAvailable()},[r]),k=(0,_react.useCallback)(async()=>{const t=await _index2.Storage.getItem(c);null!==t&&(await _index2.Storage.removeItem(c),e&&Date.now()-Number(t)>=a&&await _()&&o(!0))},[e,c,a,_]);(0,_react.useEffect)(()=>{e||o(!1),k()},[e,k]),(0,_react.useEffect)(()=>{const t=_reactNative.AppState.addEventListener("change",t=>{"background"===t&&e?_index2.Storage.setItem(c,String(Date.now())):"active"===t&&k()});return()=>t.remove()},[e,c,k]),(0,_react.useEffect)(()=>{let e=!1;return isBiometricAvailable().then(t=>{e||(u(t),p(!0))}),_().then(t=>{e||l(t)}),()=>{e=!0}},[_]);return{locked:n,enabled:i,biometricAvailable:s,availabilityKnown:d,setEnabled:(0,_react.useCallback)(async e=>await _index2.Storage.setItem(r,e?"true":"false")?(l(e),{ok:!0}):{ok:!1,reason:"failed"},[r]),unlock:(0,_react.useCallback)(async e=>{const t=await authenticateBiometric(e);return t.ok&&o(!1),t},[])}}
@@ -1 +1 @@
1
- "use strict";import{getConfigValue}from"../config/index.js";import{readAppBuild}from"../versionCheck/appBuild.js";export const CLIENT_BUILD_HEADER="Client-Build";export const APP_BUILD_HEADER="App-Build";export const CLIENT_KIND_HEADER="Client-Kind";export const ACCOUNT_ID_HEADER="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,"Client-Kind","MobileApp"),setIfAbsent(t.headers,"Client-Build",getClientBuild());const e=readAppBuild();return setIfAbsent(t.headers,"App-Build",null===e?null:String(e)),t})};export const readAccountId=async t=>t&&await t()||null;export const stampAccountId=async(t,e)=>{if(void 0!==t._accountId)return void(t._accountId&&t.headers.set("Account-Id",t._accountId));const n=t.headers.get("Account-Id");if("string"==typeof n&&""!==n)return void(t._accountId=n);const o=await readAccountId(e);t._accountId=o,o&&t.headers.set("Account-Id",o)};
1
+ "use strict";import{getConfigValue}from"../config/index.js";import{readAppBuild}from"../versionCheck/appBuild.js";export const CLIENT_BUILD_HEADER="Client-Build";export const APP_BUILD_HEADER="App-Build";export const CLIENT_KIND_HEADER="Client-Kind";export const CLIENT_APP_HEADER="Client-App";export const ACCOUNT_ID_HEADER="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,"Client-Kind","MobileApp"),setIfAbsent(t.headers,"Client-App",getConfigValue("appId")),setIfAbsent(t.headers,"Client-Build",getClientBuild());const e=readAppBuild();return setIfAbsent(t.headers,"App-Build",null===e?null:String(e)),t})};export const readAccountId=async t=>t&&await t()||null;export const stampAccountId=async(t,e)=>{if(void 0!==t._accountId)return void(t._accountId&&t.headers.set("Account-Id",t._accountId));const n=t.headers.get("Account-Id");if("string"==typeof n&&""!==n)return void(t._accountId=n);const o=await readAccountId(e);t._accountId=o,o&&t.headers.set("Account-Id",o)};
@@ -1 +1 @@
1
- "use strict";import*as LocalAuthentication from"expo-local-authentication";import{useCallback,useEffect,useState}from"react";import{AppState}from"react-native";import{Logger}from"../logger/index.js";import{Storage}from"../storage/index.js";const DEFAULT_POLICY={lockAfterMs:3e5,enabledKey:"appLockEnabled",backgroundedAtKey:"appLockBackgroundedAt"};let pendingPrompt=null;export async function isBiometricAvailable(){try{const[e,t]=await Promise.all([LocalAuthentication.hasHardwareAsync(),LocalAuthentication.isEnrolledAsync()]);return e&&t}catch(e){return Logger.error("[AppLock] biometric availability check failed:",e),!1}}const failureFor=e=>{switch(e){case"user_cancel":case"app_cancel":case"system_cancel":case"user_fallback":return"cancelled";case"lockout":return"lockout";case"not_enrolled":case"passcode_not_set":return"not-enrolled";case"not_available":return"not-available";default:return"failed"}},prompt=async e=>{try{if(await LocalAuthentication.getEnrolledLevelAsync()===LocalAuthentication.SecurityLevel.NONE)return{ok:!1,reason:"not-enrolled"};const t=await LocalAuthentication.authenticateAsync({promptMessage:e,cancelLabel:"Cancel",fallbackLabel:"Use passcode",disableDeviceFallback:!1});return t.success?{ok:!0}:(Logger.warn("[AppLock] check not passed:",t.error),{ok:!1,reason:failureFor(t.error)})}catch(e){return Logger.error("[AppLock] check failed:",e),{ok:!1,reason:"failed"}}};export function authenticateBiometric(e){return pendingPrompt||(pendingPrompt=prompt(e).finally(()=>{pendingPrompt=null})),pendingPrompt}export function useAppLock({active:e,policy:t}){const a=t?.lockAfterMs??DEFAULT_POLICY.lockAfterMs,r=t?.enabledKey??DEFAULT_POLICY.enabledKey,o=t?.backgroundedAtKey??DEFAULT_POLICY.backgroundedAtKey,[c,n]=useState(!1),[l,i]=useState(!1),[s,u]=useState(!1),p=useCallback(async()=>{const e=await Storage.getItem(r);return"true"===e||"false"===e?"true"===e:isBiometricAvailable()},[r]),d=useCallback(async()=>{const t=await Storage.getItem(o);null!==t&&(await Storage.removeItem(o),e&&Date.now()-Number(t)>=a&&await p()&&n(!0))},[e,o,a,p]);useEffect(()=>{e||n(!1),d()},[e,d]),useEffect(()=>{const t=AppState.addEventListener("change",t=>{"background"===t&&e?Storage.setItem(o,String(Date.now())):"active"===t&&d()});return()=>t.remove()},[e,o,d]),useEffect(()=>{let e=!1;return Promise.all([p(),isBiometricAvailable()]).then(([t,a])=>{e||(i(t),u(a))}),()=>{e=!0}},[p]);return{locked:c,enabled:l,biometricAvailable:s,setEnabled:useCallback(async e=>await Storage.setItem(r,e?"true":"false")?(i(e),{ok:!0}):{ok:!1,reason:"failed"},[r]),unlock:useCallback(async e=>{const t=await authenticateBiometric(e);return t.ok&&n(!1),t},[])}}
1
+ "use strict";import*as LocalAuthentication from"expo-local-authentication";import{useCallback,useEffect,useState}from"react";import{AppState}from"react-native";import{Logger}from"../logger/index.js";import{Storage}from"../storage/index.js";const DEFAULT_POLICY={lockAfterMs:3e5,enabledKey:"appLockEnabled",backgroundedAtKey:"appLockBackgroundedAt"};let pendingPrompt=null;export async function isBiometricAvailable(){try{const[e,t]=await Promise.all([LocalAuthentication.hasHardwareAsync(),LocalAuthentication.isEnrolledAsync()]);return e&&t}catch(e){return Logger.error("[AppLock] biometric availability check failed:",e),!1}}const failureFor=e=>{switch(e){case"user_cancel":case"app_cancel":case"system_cancel":case"user_fallback":return"cancelled";case"lockout":return"lockout";case"not_enrolled":case"passcode_not_set":return"not-enrolled";case"not_available":return"not-available";default:return"failed"}},prompt=async e=>{try{if(await LocalAuthentication.getEnrolledLevelAsync()===LocalAuthentication.SecurityLevel.NONE)return{ok:!1,reason:"not-enrolled"};const t=await LocalAuthentication.authenticateAsync({promptMessage:e,cancelLabel:"Cancel",fallbackLabel:"Use passcode",disableDeviceFallback:!1});return t.success?{ok:!0}:(Logger.warn("[AppLock] check not passed:",t.error),{ok:!1,reason:failureFor(t.error)})}catch(e){return Logger.error("[AppLock] check failed:",e),{ok:!1,reason:"failed"}}};export function authenticateBiometric(e){return pendingPrompt||(pendingPrompt=prompt(e).finally(()=>{pendingPrompt=null})),pendingPrompt}export function useAppLock({active:e,policy:t}){const a=t?.lockAfterMs??DEFAULT_POLICY.lockAfterMs,o=t?.enabledKey??DEFAULT_POLICY.enabledKey,r=t?.backgroundedAtKey??DEFAULT_POLICY.backgroundedAtKey,[n,c]=useState(!1),[l,i]=useState(!1),[s,u]=useState(!1),[p,d]=useState(!1),k=useCallback(async()=>{const e=await Storage.getItem(o);return"true"===e||"false"===e?"true"===e:isBiometricAvailable()},[o]),m=useCallback(async()=>{const t=await Storage.getItem(r);null!==t&&(await Storage.removeItem(r),e&&Date.now()-Number(t)>=a&&await k()&&c(!0))},[e,r,a,k]);useEffect(()=>{e||c(!1),m()},[e,m]),useEffect(()=>{const t=AppState.addEventListener("change",t=>{"background"===t&&e?Storage.setItem(r,String(Date.now())):"active"===t&&m()});return()=>t.remove()},[e,r,m]),useEffect(()=>{let e=!1;return isBiometricAvailable().then(t=>{e||(u(t),d(!0))}),k().then(t=>{e||i(t)}),()=>{e=!0}},[k]);return{locked:n,enabled:l,biometricAvailable:s,availabilityKnown:p,setEnabled:useCallback(async e=>await Storage.setItem(o,e?"true":"false")?(i(e),{ok:!0}):{ok:!1,reason:"failed"},[o]),unlock:useCallback(async e=>{const t=await authenticateBiometric(e);return t.ok&&c(!1),t},[])}}
@@ -2,6 +2,7 @@ import type { AxiosInstance } from "axios";
2
2
  export declare const CLIENT_BUILD_HEADER = "Client-Build";
3
3
  export declare const APP_BUILD_HEADER = "App-Build";
4
4
  export declare const CLIENT_KIND_HEADER = "Client-Kind";
5
+ export declare const CLIENT_APP_HEADER = "Client-App";
5
6
  export declare const ACCOUNT_ID_HEADER = "Account-Id";
6
7
  /** The server picks session rules and the minimum version per kind; the names match its enum. */
7
8
  export type ClientKind = "Browser" | "MobileApp";
@@ -9,7 +10,8 @@ export declare const CLIENT_KIND: ClientKind;
9
10
  /** `<appVersion>+<commit>`, `<appVersion>` when no commit was stamped, `null` before init. */
10
11
  export declare const getClientBuild: () => string | null;
11
12
  /**
12
- * Stamps Client-Kind, Client-Build and App-Build on every request.
13
+ * Stamps Client-Kind, Client-Build and App-Build on every request, and Client-App with the
14
+ * config's app id when one is set, so the server can tell the apps of one kind apart.
13
15
  * App-Build is the whole build number the server compares with its minimum;
14
16
  * it is left off when the build is unknown, because the server never refuses a
15
17
  * request without it and a guessed number could strand a current build. A value the app
@@ -14,6 +14,8 @@ export interface AppLockState {
14
14
  locked: boolean;
15
15
  enabled: boolean;
16
16
  biometricAvailable: boolean;
17
+ /** False until the first biometric availability check settles, so `biometricAvailable` is not yet a real answer. */
18
+ availabilityKnown: boolean;
17
19
  setEnabled: (enabled: boolean) => Promise<AppLockResult>;
18
20
  unlock: (reason: string) => Promise<AppLockResult>;
19
21
  }
@@ -25,6 +27,8 @@ export declare function authenticateBiometric(reason: string): Promise<AppLockRe
25
27
  * The time it left the screen is saved, not held in memory, so a phone that closed the app in the background still
26
28
  * locks on the next launch. Only "background" counts as away: iOS reports "inactive" while the Face ID prompt is up.
27
29
  * The person's choice is stored under `enabledKey`; with none saved it is on when the phone has a fingerprint or face enrolled.
30
+ * `availabilityKnown` stays false until the first availability check settles, so a screen can wait before it reads
31
+ * `biometricAvailable` as "this phone has no biometrics".
28
32
  */
29
33
  export declare function useAppLock({ active, policy }: {
30
34
  active: boolean;
@@ -2,6 +2,7 @@ import type { AxiosInstance } from "axios";
2
2
  export declare const CLIENT_BUILD_HEADER = "Client-Build";
3
3
  export declare const APP_BUILD_HEADER = "App-Build";
4
4
  export declare const CLIENT_KIND_HEADER = "Client-Kind";
5
+ export declare const CLIENT_APP_HEADER = "Client-App";
5
6
  export declare const ACCOUNT_ID_HEADER = "Account-Id";
6
7
  /** The server picks session rules and the minimum version per kind; the names match its enum. */
7
8
  export type ClientKind = "Browser" | "MobileApp";
@@ -9,7 +10,8 @@ export declare const CLIENT_KIND: ClientKind;
9
10
  /** `<appVersion>+<commit>`, `<appVersion>` when no commit was stamped, `null` before init. */
10
11
  export declare const getClientBuild: () => string | null;
11
12
  /**
12
- * Stamps Client-Kind, Client-Build and App-Build on every request.
13
+ * Stamps Client-Kind, Client-Build and App-Build on every request, and Client-App with the
14
+ * config's app id when one is set, so the server can tell the apps of one kind apart.
13
15
  * App-Build is the whole build number the server compares with its minimum;
14
16
  * it is left off when the build is unknown, because the server never refuses a
15
17
  * request without it and a guessed number could strand a current build. A value the app
@@ -14,6 +14,8 @@ export interface AppLockState {
14
14
  locked: boolean;
15
15
  enabled: boolean;
16
16
  biometricAvailable: boolean;
17
+ /** False until the first biometric availability check settles, so `biometricAvailable` is not yet a real answer. */
18
+ availabilityKnown: boolean;
17
19
  setEnabled: (enabled: boolean) => Promise<AppLockResult>;
18
20
  unlock: (reason: string) => Promise<AppLockResult>;
19
21
  }
@@ -25,6 +27,8 @@ export declare function authenticateBiometric(reason: string): Promise<AppLockRe
25
27
  * The time it left the screen is saved, not held in memory, so a phone that closed the app in the background still
26
28
  * locks on the next launch. Only "background" counts as away: iOS reports "inactive" while the Face ID prompt is up.
27
29
  * The person's choice is stored under `enabledKey`; with none saved it is on when the phone has a fingerprint or face enrolled.
30
+ * `availabilityKnown` stays false until the first availability check settles, so a screen can wait before it reads
31
+ * `biometricAvailable` as "this phone has no biometrics".
28
32
  */
29
33
  export declare function useAppLock({ active, policy }: {
30
34
  active: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/mobile-core",
3
- "version": "0.112.3",
3
+ "version": "0.114.0",
4
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",