@webority/mobile-core 0.2.2 → 0.4.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
@@ -73,11 +73,26 @@ error toast middleware skips.
73
73
  peer, and reports `unavailable` without it or in Expo Go. Call `initPush` once at startup with
74
74
  `getInstallationId`, `register`, `unregister` and `onOpen`; `enablePush` asks permission, sends
75
75
  the native token (`Fcm` on Android, `Apns` on iOS) and the bundle id to `register`, and
76
- re-registers when the token changes; `disablePush` calls `unregister`. `onOpen` receives the
76
+ re-registers when the token changes; `disablePush` calls `unregister` and turns push off only once
77
+ it succeeds (a failure rejects and leaves push on). `resetPush` turns push off on the phone with no
78
+ server call and drops the native token, for a sign-out with no session left. `onOpen` receives the
77
79
  tapped notification's data as strings, including the tap that launched the app. `usePush` gives a
78
80
  settings switch its `status` and `enabled`. The library does not remember the switch: an app that
79
81
  keeps push on calls `enablePush` at each start. `setBadgeCount(count)` sets the app icon badge.
80
82
 
83
+ - `enablePush({ prompt: false })` never shows the permission prompt: a phone already granted
84
+ registers silently, and any other resolves with its status (`unknown` when the person has not
85
+ been asked yet, `denied` when refused). Call it at sign-in, and call `enablePush()` when the
86
+ person reaches a moment where asking makes sense.
87
+ - Every registered device carries `apnsEnvironment`: `Development` or `Production` on iOS, read
88
+ from the build's `aps-environment` (expo-application), so the server sends each phone to the
89
+ APNs gateway its signing accepts. It is null on Android, and on iOS when it cannot be read (the
90
+ simulator); the server then uses its own setting.
91
+ - `onReceive(handler)` calls `handler({ notificationId, title, body, data })` for each
92
+ notification that arrives while the app is open, shown or not, so the app can refresh a list or
93
+ an unread count. It returns a function that removes the handler. A notification arriving in the
94
+ background does not reach it; its tap still goes to `onOpen`.
95
+
81
96
  ### Notification buttons
82
97
 
83
98
  A push names a category; the category's buttons are registered on the phone. Register them at
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;var _axios=_interopRequireDefault(require("axios")),_index=require("../config/index.js"),_index2=require("../logger/index.js");require("../types/axios.js");var _baseUrl=require("./baseUrl.js"),_clientHeaders=require("./clientHeaders.js"),_curl=require("./curl.js"),_minAppVersion=require("./minAppVersion.js"),_multipart=require("./multipart.js"),_requestDefaults=require("./requestDefaults.js"),_responseBodyText=require("./responseBodyText.js"),_retry=require("./retry.js");function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}const publicClient=_axios.default.create({headers:{"Content-Type":"application/json",Accept:"application/json"}});(0,_baseUrl.applyConfiguredBaseUrl)(publicClient),(0,_requestDefaults.applyRequestDefaults)(publicClient),(0,_multipart.sendFormDataAsMultipart)(publicClient),(0,_minAppVersion.readMinAppVersion)(publicClient),(0,_retry.attachRetry)(publicClient),publicClient.interceptors.request.use(e=>(_index2.Logger.info(`Request: ${e.method?.toUpperCase()} ${e.url}`),(0,_index.getConfigValue)("logCurl")&&(0,_curl.logCurl)(e),e),e=>{const r=e instanceof Error?e.message:String(e);return _index2.Logger.error("Request error:",r),Promise.reject(e)}),(0,_clientHeaders.applyClientHeaders)(publicClient),publicClient.interceptors.response.use(e=>(_index2.Logger.info(`Response: ${e.status} from ${e.config.url}`),e),e=>{if(_axios.default.isAxiosError(e)){const r=e.config;if(r?.skipErrorLog)return Promise.reject(e);e.response?_index2.Logger.error(`Response error: ${(0,_responseBodyText.responseBodyText)(e.response.data,e.message)}, URL: ${r?.url}, Status: ${e.response.status}`):_index2.Logger.error(`Network error or no response from server: ${e.message}, URL: ${r?.url}`)}else{const r=e instanceof Error?e.message:String(e);_index2.Logger.error(`Unknown response error: ${r}`)}return Promise.reject(e)});var _default=exports.default=publicClient;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=void 0;var _axios=_interopRequireDefault(require("axios")),_index=require("../config/index.js"),_index2=require("../logger/index.js");require("../types/axios.js");var _baseUrl=require("./baseUrl.js"),_clientHeaders=require("./clientHeaders.js"),_curl=require("./curl.js"),_minAppVersion=require("./minAppVersion.js"),_multipart=require("./multipart.js"),_requestDefaults=require("./requestDefaults.js"),_responseBodyText=require("./responseBodyText.js"),_retry=require("./retry.js");function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}const publicClient=_axios.default.create({headers:{"Content-Type":"application/json",Accept:"application/json"}});(0,_baseUrl.applyConfiguredBaseUrl)(publicClient),(0,_requestDefaults.applyRequestDefaults)(publicClient),(0,_multipart.sendFormDataAsMultipart)(publicClient),(0,_minAppVersion.readMinAppVersion)(publicClient),(0,_retry.attachRetry)(publicClient),publicClient.interceptors.request.use(e=>(_index2.Logger.info(`Request: ${e.method?.toUpperCase()} ${e.url}`),(0,_index.getConfigValue)("logCurl")&&(0,_curl.logCurl)(e),e),e=>{const r=e instanceof Error?e.message:String(e);return _index2.Logger.error("Request error:",r),Promise.reject(e)}),(0,_clientHeaders.applyClientHeaders)(publicClient),publicClient.interceptors.response.use(e=>(_index2.Logger.info(`Response: ${e.status} from ${e.config.url}`),e),e=>{if(_axios.default.isCancel(e))return Promise.reject(e);if(_axios.default.isAxiosError(e)){const r=e.config;if(r?.skipErrorLog)return Promise.reject(e);e.response?_index2.Logger.error(`Response error: ${(0,_responseBodyText.responseBodyText)(e.response.data,e.message)}, URL: ${r?.url}, Status: ${e.response.status}`):_index2.Logger.error(`Network error or no response from server: ${e.message}, URL: ${r?.url}`)}else{const r=e instanceof Error?e.message:String(e);_index2.Logger.error(`Unknown response error: ${r}`)}return Promise.reject(e)});var _default=exports.default=publicClient;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.SESSION_TERMINATION_CODES=exports.SESSION_INVALID_HEADER=exports.RequestOfflineError=void 0,Object.defineProperty(exports,"SIGN_OUT_REASON_HEADER",{enumerable:!0,get:function(){return _tokenRefresh.SIGN_OUT_REASON_HEADER}}),exports.default=void 0,Object.defineProperty(exports,"injectStore",{enumerable:!0,get:function(){return _tokenRefresh.injectStore}}),Object.defineProperty(exports,"injectUserAuth",{enumerable:!0,get:function(){return _tokenRefresh.injectUserAuth}}),Object.defineProperty(exports,"resetRefreshState",{enumerable:!0,get:function(){return _tokenRefresh.resetRefreshState}});var _axios=_interopRequireWildcard(require("axios")),_authStore=require("../auth/authStore.js"),_index=require("../config/index.js"),_index2=require("../logger/index.js"),_networkStatus=require("../network/networkStatus.js");require("../types/axios.js");var _baseUrl=require("./baseUrl.js"),_clientHeaders=require("./clientHeaders.js"),_curl=require("./curl.js"),_minAppVersion=require("./minAppVersion.js"),_multipart=require("./multipart.js"),_requestDefaults=require("./requestDefaults.js"),_responseBodyText=require("./responseBodyText.js"),_retry=require("./retry.js"),_tokenRefresh=require("./tokenRefresh.js");function _interopRequireWildcard(e,r){if("function"==typeof WeakMap)var t=new WeakMap,s=new WeakMap;return(_interopRequireWildcard=function(e,r){if(!r&&e&&e.__esModule)return e;var o,n,i={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return i;if(o=r?s:t){if(o.has(e))return o.get(e);o.set(e,i)}for(const r in e)"default"!==r&&{}.hasOwnProperty.call(e,r)&&((n=(o=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,r))&&(n.get||n.set)?o(i,r,n):i[r]=e[r]);return i})(e,r)}class RequestOfflineError extends Error{constructor(e="Request blocked: device is offline"){super(e),this.name="RequestOfflineError",Object.setPrototypeOf(this,RequestOfflineError.prototype)}}exports.RequestOfflineError=RequestOfflineError;const SESSION_TERMINATION_CODES=exports.SESSION_TERMINATION_CODES={TERMINATED:"SESSION_TERMINATED",EXPIRED:"SESSION_EXPIRED"},TERMINATION_CODES=new Set(Object.values(SESSION_TERMINATION_CODES)),SESSION_INVALID_HEADER=exports.SESSION_INVALID_HEADER="x-security-stamp-invalid",hasSessionInvalidHeader=e=>{if(!e||"object"!=typeof e)return!1;const r=e.get,t="function"==typeof r?r.call(e,SESSION_INVALID_HEADER):e[SESSION_INVALID_HEADER];return"true"===String(t).toLowerCase()},getSessionTerminationCode=e=>{if(!e||"object"!=typeof e)return;const r=e,t=r.error,s=[r.code,r.errorCode,t&&"object"==typeof t?t.code:void 0];for(const e of s)if("string"==typeof e&&TERMINATION_CODES.has(e))return e},userClient=_axios.default.create({headers:{"Content-Type":"application/json"}});(0,_minAppVersion.readMinAppVersion)(userClient),(0,_retry.attachRetry)(userClient);const setAuthHeader=(e,r)=>{e.headers instanceof _axios.AxiosHeaders?e.headers.set("Authorization",`Bearer ${r}`):e.headers=_axios.AxiosHeaders.from({...e.headers,Authorization:`Bearer ${r}`})},isRefreshTokenRequest=e=>{const r=(0,_tokenRefresh.getRefreshPath)();return!!r&&!!e?.includes(r)};(0,_baseUrl.applyConfiguredBaseUrl)(userClient),(0,_requestDefaults.applyRequestDefaults)(userClient),(0,_multipart.sendFormDataAsMultipart)(userClient),userClient.interceptors.request.use(e=>((0,_index.getConfigValue)("logCurl")&&(0,_curl.logCurl)(e),e)),(0,_clientHeaders.applyClientHeaders)(userClient),userClient.interceptors.request.use(async e=>{if(!1===(await(0,_networkStatus.getLastKnownNetworkStatus)()).isConnected)return _index2.Logger.warn(`Request blocked: device is offline. URL: ${e.url}`),Promise.reject(new RequestOfflineError);try{if(isRefreshTokenRequest(e.url))return e;const r=await(0,_authStore.getToken)(_authStore.ACCESS_TOKEN_KEY);if(!r?.token)throw new Error("No access token available");const t=Date.now(),s=Date.parse(r.expiry);if(s<=t+3e4){_index2.Logger.info("Access token expired or about to expire, refreshing before request");const r=await(0,_tokenRefresh.refreshAccessToken)();return setAuthHeader(e,r),await(0,_clientHeaders.applyAccountIdHeader)(e.headers,(0,_tokenRefresh.getAccountIdProvider)()),e}return setAuthHeader(e,r.token),await(0,_clientHeaders.applyAccountIdHeader)(e.headers,(0,_tokenRefresh.getAccountIdProvider)()),e}catch(e){return _index2.Logger.error("Failed to attach token:",(0,_tokenRefresh.getErrorMessage)(e)),Promise.reject(e)}},e=>(_index2.Logger.error("Request error:",(0,_tokenRefresh.getErrorMessage)(e)),Promise.reject(e))),userClient.interceptors.response.use(e=>e,async e=>{if(!_axios.default.isAxiosError(e))return _index2.Logger.error(`Non-axios error in response interceptor: ${(0,_tokenRefresh.getErrorMessage)(e)}`),Promise.reject(e);const r=e,t=r.config;if(!r.response)return t?.skipErrorLog||_index2.Logger.error(`Network error or no response from server: ${r.message}, URL: ${t?.url}`),Promise.reject(e);if(401===r.response.status&&t&&!t._retry){if(isRefreshTokenRequest(t.url))return Promise.reject(e);const s=(0,_tokenRefresh.readSignOutReason)(r.response.headers),o=getSessionTerminationCode(r.response.data)??(hasSessionInvalidHeader(r.response.headers)||null!==s?SESSION_TERMINATION_CODES.TERMINATED:void 0);if(o)return _index2.Logger.warn(`Session ${o}, logging out without refresh. URL: ${t.url}`),(0,_tokenRefresh.resetRefreshState)(),await(0,_tokenRefresh.endSessionAndLogout)("terminated",s),Promise.reject(e);if(_index2.Logger.warn(`Unauthorized URL: ${t.url}`),t._retry=!0,(0,_tokenRefresh.isRefreshInFlight)())return(0,_tokenRefresh.queueBehindRefresh)().then(e=>(_index2.Logger.info("Retrying request after token refresh:",t.url),setAuthHeader(t,e),userClient(t))).catch(e=>(_index2.Logger.error("Failed to retry request after token refresh:",(0,_tokenRefresh.getErrorMessage)(e)),Promise.reject(e)));try{const e=await(0,_tokenRefresh.refreshAccessToken)();return setAuthHeader(t,e),userClient(t)}catch(e){return Promise.reject(e)}}return t?.skipErrorLog||_index2.Logger.error(`Response error: ${(0,_responseBodyText.responseBodyText)(r.response?.data,r.message)}, URL: ${t?.url}, Status: ${r.response.status}`),Promise.reject(e)});var _default=exports.default=userClient;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.SESSION_TERMINATION_CODES=exports.SESSION_INVALID_HEADER=exports.RequestOfflineError=void 0,Object.defineProperty(exports,"SIGN_OUT_REASON_HEADER",{enumerable:!0,get:function(){return _tokenRefresh.SIGN_OUT_REASON_HEADER}}),exports.default=void 0,Object.defineProperty(exports,"injectStore",{enumerable:!0,get:function(){return _tokenRefresh.injectStore}}),Object.defineProperty(exports,"injectUserAuth",{enumerable:!0,get:function(){return _tokenRefresh.injectUserAuth}}),Object.defineProperty(exports,"resetRefreshState",{enumerable:!0,get:function(){return _tokenRefresh.resetRefreshState}});var _axios=_interopRequireWildcard(require("axios")),_authStore=require("../auth/authStore.js"),_index=require("../config/index.js"),_index2=require("../logger/index.js"),_networkStatus=require("../network/networkStatus.js");require("../types/axios.js");var _baseUrl=require("./baseUrl.js"),_clientHeaders=require("./clientHeaders.js"),_curl=require("./curl.js"),_minAppVersion=require("./minAppVersion.js"),_multipart=require("./multipart.js"),_requestDefaults=require("./requestDefaults.js"),_responseBodyText=require("./responseBodyText.js"),_retry=require("./retry.js"),_tokenRefresh=require("./tokenRefresh.js");function _interopRequireWildcard(e,r){if("function"==typeof WeakMap)var t=new WeakMap,s=new WeakMap;return(_interopRequireWildcard=function(e,r){if(!r&&e&&e.__esModule)return e;var o,n,i={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return i;if(o=r?s:t){if(o.has(e))return o.get(e);o.set(e,i)}for(const r in e)"default"!==r&&{}.hasOwnProperty.call(e,r)&&((n=(o=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,r))&&(n.get||n.set)?o(i,r,n):i[r]=e[r]);return i})(e,r)}class RequestOfflineError extends Error{constructor(e="Request blocked: device is offline"){super(e),this.name="RequestOfflineError",Object.setPrototypeOf(this,RequestOfflineError.prototype)}}exports.RequestOfflineError=RequestOfflineError;class NoAccessTokenError extends Error{constructor(){super("No access token available"),this.name="NoAccessTokenError",Object.setPrototypeOf(this,NoAccessTokenError.prototype)}}const SESSION_TERMINATION_CODES=exports.SESSION_TERMINATION_CODES={TERMINATED:"SESSION_TERMINATED",EXPIRED:"SESSION_EXPIRED"},TERMINATION_CODES=new Set(Object.values(SESSION_TERMINATION_CODES)),SESSION_INVALID_HEADER=exports.SESSION_INVALID_HEADER="x-security-stamp-invalid",hasSessionInvalidHeader=e=>{if(!e||"object"!=typeof e)return!1;const r=e.get,t="function"==typeof r?r.call(e,SESSION_INVALID_HEADER):e[SESSION_INVALID_HEADER];return"true"===String(t).toLowerCase()},getSessionTerminationCode=e=>{if(!e||"object"!=typeof e)return;const r=e,t=r.error,s=[r.code,r.errorCode,t&&"object"==typeof t?t.code:void 0];for(const e of s)if("string"==typeof e&&TERMINATION_CODES.has(e))return e},userClient=_axios.default.create({headers:{"Content-Type":"application/json"}});(0,_minAppVersion.readMinAppVersion)(userClient),(0,_retry.attachRetry)(userClient);const setAuthHeader=(e,r)=>{e.headers instanceof _axios.AxiosHeaders?e.headers.set("Authorization",`Bearer ${r}`):e.headers=_axios.AxiosHeaders.from({...e.headers,Authorization:`Bearer ${r}`})},isRefreshTokenRequest=e=>{const r=(0,_tokenRefresh.getRefreshPath)();return!!r&&!!e?.includes(r)};(0,_baseUrl.applyConfiguredBaseUrl)(userClient),(0,_requestDefaults.applyRequestDefaults)(userClient),(0,_multipart.sendFormDataAsMultipart)(userClient),userClient.interceptors.request.use(e=>((0,_index.getConfigValue)("logCurl")&&(0,_curl.logCurl)(e),e)),(0,_clientHeaders.applyClientHeaders)(userClient),userClient.interceptors.request.use(async e=>{if(!1===(await(0,_networkStatus.getLastKnownNetworkStatus)()).isConnected)return _index2.Logger.warn(`Request blocked: device is offline. URL: ${e.url}`),Promise.reject(new RequestOfflineError);try{if(isRefreshTokenRequest(e.url))return e;const r=await(0,_authStore.getToken)(_authStore.ACCESS_TOKEN_KEY);if(!r?.token)throw new NoAccessTokenError;const t=Date.now(),s=Date.parse(r.expiry);if(s<=t+3e4){_index2.Logger.info("Access token expired or about to expire, refreshing before request");const r=await(0,_tokenRefresh.refreshAccessToken)();return setAuthHeader(e,r),await(0,_clientHeaders.applyAccountIdHeader)(e.headers,(0,_tokenRefresh.getAccountIdProvider)()),e}return setAuthHeader(e,r.token),await(0,_clientHeaders.applyAccountIdHeader)(e.headers,(0,_tokenRefresh.getAccountIdProvider)()),e}catch(r){return r instanceof NoAccessTokenError?_index2.Logger.info(`Request not sent: signed out. URL: ${e.url}`):_index2.Logger.error("Failed to attach token:",(0,_tokenRefresh.getErrorMessage)(r)),Promise.reject(r)}},e=>(_index2.Logger.error("Request error:",(0,_tokenRefresh.getErrorMessage)(e)),Promise.reject(e))),userClient.interceptors.response.use(e=>e,async e=>{if(_axios.default.isCancel(e))return Promise.reject(e);if(e instanceof RequestOfflineError||e instanceof NoAccessTokenError)return Promise.reject(e);if(!_axios.default.isAxiosError(e))return _index2.Logger.error(`Non-axios error in response interceptor: ${(0,_tokenRefresh.getErrorMessage)(e)}`),Promise.reject(e);const r=e,t=r.config;if(!r.response)return t?.skipErrorLog||_index2.Logger.error(`Network error or no response from server: ${r.message}, URL: ${t?.url}`),Promise.reject(e);if(401===r.response.status&&t&&!t._retry){if(isRefreshTokenRequest(t.url))return Promise.reject(e);const s=(0,_tokenRefresh.readSignOutReason)(r.response.headers),o=getSessionTerminationCode(r.response.data)??(hasSessionInvalidHeader(r.response.headers)||null!==s?SESSION_TERMINATION_CODES.TERMINATED:void 0);if(o)return _index2.Logger.warn(`Session ${o}, logging out without refresh. URL: ${t.url}`),(0,_tokenRefresh.resetRefreshState)(),await(0,_tokenRefresh.endSessionAndLogout)("terminated",s),Promise.reject(e);if(_index2.Logger.warn(`Unauthorized URL: ${t.url}`),t._retry=!0,(0,_tokenRefresh.isRefreshInFlight)())return(0,_tokenRefresh.queueBehindRefresh)().then(e=>(_index2.Logger.info("Retrying request after token refresh:",t.url),setAuthHeader(t,e),userClient(t))).catch(e=>(_index2.Logger.error("Failed to retry request after token refresh:",(0,_tokenRefresh.getErrorMessage)(e)),Promise.reject(e)));try{const e=await(0,_tokenRefresh.refreshAccessToken)();return setAuthHeader(t,e),userClient(t)}catch(e){return Promise.reject(e)}}return t?.skipErrorLog||_index2.Logger.error(`Response error: ${(0,_responseBodyText.responseBodyText)(r.response?.data,r.message)}, URL: ${t?.url}, Status: ${r.response.status}`),Promise.reject(e)});var _default=exports.default=userClient;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.setRemoteLogger=exports.setDeviceContext=exports.getDeviceContext=exports.configureLogger=exports.Logger=void 0;var _index=require("../config/index.js"),_redact=require("./redact.js");const Colors={RESET:"",GRAY:"",BLUE:"",YELLOW:"",RED_BOLD:""},LogLevels={DEBUG:{level:"debug",color:Colors.GRAY},INFO:{level:"info",color:Colors.BLUE},WARN:{level:"warn",color:Colors.YELLOW},ERROR:{level:"error",color:Colors.RED_BOLD}},isLogLevelAllowed=e=>{const r=["debug","info","warn","error"],o=(0,_index.getConfigValue)("logLevel","info");return r.indexOf(e)>=r.indexOf(o)},isEnabled=()=>(0,_index.getConfigValue)("loggingEnabled",!0);let remote=null;const setRemoteLogger=e=>{remote=e};exports.setRemoteLogger=setRemoteLogger;const safe=e=>{if(e)try{e()}catch{}},toErrorObject=e=>{if(e instanceof Error)return e;if("string"==typeof e)return new Error(e);try{return new Error(JSON.stringify(e))}catch{return new Error(String(e))}};let deviceContext=null;const normalizeAttributeValue=e=>null==e?null:"string"==typeof e||"number"==typeof e||"boolean"==typeof e?e:String(e),setDeviceContext=e=>{if(deviceContext=e,e)for(const[r,o]of Object.entries(e))safe(()=>remote?.setAttribute?.(r,normalizeAttributeValue(o)))};exports.setDeviceContext=setDeviceContext;const getDeviceContext=()=>deviceContext;exports.getDeviceContext=getDeviceContext;const DEFAULT_LOGGER_OPTIONS={dedupeWindowMs:5e3,dedupeMaxPerWindow:1,screenHistoryLimit:20};let loggerOptions={...DEFAULT_LOGGER_OPTIONS};const configureLogger=e=>{loggerOptions={...loggerOptions,...e},screenHistory.length>loggerOptions.screenHistoryLimit&&(screenHistory=screenHistory.slice(screenHistory.length-loggerOptions.screenHistoryLimit))};exports.configureLogger=configureLogger;let currentScreen=null,screenHistory=[];const dedupeMap=new Map,MAX_DEDUPE_ENTRIES=200,dedupeKeyFor=e=>{const r=e.stack?.split("\n")[1]?.trim();return r?`${e.message}::${r}`:e.message},capDedupeMap=()=>{for(;dedupeMap.size>200;){const e=dedupeMap.keys().next().value;if(void 0===e)return;dedupeMap.delete(e)}},shouldForward=e=>{if(loggerOptions.dedupeWindowMs<=0)return{forward:!0};const r=Date.now(),o=dedupeMap.get(e);if(!o||r-o.windowStart>=loggerOptions.dedupeWindowMs){const t=o?.suppressedCount??0;return dedupeMap.set(e,{windowStart:r,count:1,suppressedCount:0}),capDedupeMap(),t>0?{forward:!0,suppressedCount:t}:{forward:!0}}return o.count+=1,o.count>loggerOptions.dedupeMaxPerWindow?(o.suppressedCount+=1,{forward:!1}):{forward:!0}},buildErrorContext=(e,r)=>{const o={...deviceContext};return null!==currentScreen&&(o.screen=currentScreen),r&&(o.suppressedCount=r),{...o,...e}},forwardErrorToRemote=(e,r)=>{const o=dedupeKeyFor(e),{forward:t,suppressedCount:s}=shouldForward(o);t&&safe(()=>remote?.recordError?.(e,buildErrorContext(r,s)))},logDebug=(e,...r)=>{isEnabled()&&isLogLevelAllowed(LogLevels.DEBUG.level)&&console.debug(`${LogLevels.DEBUG.color}[DEBUG]: ${(0,_redact.redactString)(e)}${Colors.RESET}`,...(0,_redact.redact)(r))},logInfo=(e,...r)=>{const o=(0,_redact.redactString)(e),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed(LogLevels.INFO.level)&&(console.info(`${LogLevels.INFO.color}[INFO]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("info",o,t)))},logWarning=(e,...r)=>{const o=(0,_redact.redactString)(e),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed(LogLevels.WARN.level)&&(console.warn(`${LogLevels.WARN.color}[WARNING]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("warn",o,t)))},logError=(e,...r)=>{const o=(0,_redact.redactString)(e),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed(LogLevels.ERROR.level)&&console.error(`${LogLevels.ERROR.color}[ERROR]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("error",o,t));const s=t[0];void 0!==s&&forwardErrorToRemote(toErrorObject(s),{source:"Logger.error",message:o})},breadcrumb=(e,r)=>{const o=(0,_redact.redactString)(e),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed("info")&&console.info(`${Colors.BLUE}[BREADCRUMB]: ${o}${Colors.RESET}`,t??""),safe(()=>remote?.breadcrumb?.(o,t))},recordError=(e,r)=>{const o=(0,_redact.redact)(toErrorObject(e)),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed("error")&&console.error(`${Colors.RED_BOLD}[RECORDED]: ${o.message}${Colors.RESET}`,t??""),forwardErrorToRemote(o,t)},asDict=e=>"object"==typeof e&&null!==e?e:null,headerValue=(e,r)=>{const o=asDict(e);if(!o)return null;const t="function"==typeof o.get?o.get.call(e,r):o[r]??o[r.toLowerCase()];return"string"==typeof t&&""!==t?t:null},requestFailed=(e,r={})=>{const{label:o,expectedStatuses:t=[],...s}=r,n=asDict(e),l=asDict(n?.config),c=asDict(n?.response),i=asDict(c?.data),d="number"==typeof c?.status?c.status:null,a="string"==typeof l?.method?l.method.toUpperCase():null,g="string"==typeof l?.url?l.url.split("?")[0]??null:null,u=o??([a,g].filter(Boolean).join(" ")||"Request");if("CanceledError"===n?.name||"ERR_CANCELED"===n?.code||!0===n?.__CANCEL__)return void logInfo(`${u} canceled`);if(null!==d&&t.includes(d))return void logWarning(`${u} refused (${d})`);const p="string"==typeof n?.message?n.message:e instanceof Error?e.message:null,E=(0,_redact.redactString)(null===d?`${u} failed`:`${u} failed (${d})`);isEnabled()&&isLogLevelAllowed(LogLevels.ERROR.level)&&console.error(`${LogLevels.ERROR.color}[ERROR]: ${E}${Colors.RESET}`,(0,_redact.redactString)(p??"")),safe(()=>remote?.log?.("error",E,[]));const f=(0,_index.getConfigValue)("correlationIdHeader",null),L=(0,_redact.redact)({source:"Logger.requestFailed",label:u,status:d,method:a,path:g,code:"string"==typeof i?.code?i.code:null,traceId:"string"==typeof i?.traceId?i.traceId:null,correlationId:f?headerValue(l?.headers,f):null,detail:p,...s});forwardErrorToRemote((0,_redact.redact)(new Error(E)),L)},setUser=(e,r)=>{safe(()=>remote?.setUser?.(e,r))},setAttribute=(e,r)=>{safe(()=>remote?.setAttribute?.(e,r))},trackScreen=(e,r)=>{e!==currentScreen&&(currentScreen=e,screenHistory.push(e),screenHistory.length>loggerOptions.screenHistoryLimit&&(screenHistory=screenHistory.slice(screenHistory.length-loggerOptions.screenHistoryLimit)),breadcrumb(`Screen: ${e}`,r))},getCurrentScreen=()=>currentScreen,getScreenHistory=()=>[...screenHistory],Logger=exports.Logger={debug:logDebug,info:logInfo,warn:logWarning,error:logError,breadcrumb:breadcrumb,recordError:recordError,requestFailed:requestFailed,setUser:setUser,setAttribute:setAttribute,trackScreen:trackScreen,getCurrentScreen:getCurrentScreen,getScreenHistory:getScreenHistory};
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.setRemoteLogger=exports.setDeviceContext=exports.getDeviceContext=exports.configureLogger=exports.Logger=void 0;var _index=require("../config/index.js"),_redact=require("./redact.js");const Colors={RESET:"",GRAY:"",BLUE:"",YELLOW:"",RED_BOLD:""},LogLevels={DEBUG:{level:"debug",color:Colors.GRAY},INFO:{level:"info",color:Colors.BLUE},WARN:{level:"warn",color:Colors.YELLOW},ERROR:{level:"error",color:Colors.RED_BOLD}},isLogLevelAllowed=e=>{const r=["debug","info","warn","error"],o=(0,_index.getConfigValue)("logLevel","info");return r.indexOf(e)>=r.indexOf(o)},isEnabled=()=>(0,_index.getConfigValue)("loggingEnabled",!0);let remote=null;const setRemoteLogger=e=>{remote=e};exports.setRemoteLogger=setRemoteLogger;const safe=e=>{if(e)try{e()}catch{}},toErrorObject=e=>{if(e instanceof Error)return e;if("string"==typeof e)return new Error(e);try{return new Error(JSON.stringify(e))}catch{return new Error(String(e))}};let deviceContext=null;const normalizeAttributeValue=e=>null==e?null:"string"==typeof e||"number"==typeof e||"boolean"==typeof e?e:String(e),setDeviceContext=e=>{if(deviceContext=e,e)for(const[r,o]of Object.entries(e))safe(()=>remote?.setAttribute?.(r,normalizeAttributeValue(o)))};exports.setDeviceContext=setDeviceContext;const getDeviceContext=()=>deviceContext;exports.getDeviceContext=getDeviceContext;const DEFAULT_LOGGER_OPTIONS={dedupeWindowMs:5e3,dedupeMaxPerWindow:1,screenHistoryLimit:20};let loggerOptions={...DEFAULT_LOGGER_OPTIONS};const configureLogger=e=>{loggerOptions={...loggerOptions,...e},screenHistory.length>loggerOptions.screenHistoryLimit&&(screenHistory=screenHistory.slice(screenHistory.length-loggerOptions.screenHistoryLimit))};exports.configureLogger=configureLogger;let currentScreen=null,screenHistory=[];const dedupeMap=new Map,MAX_DEDUPE_ENTRIES=200,dedupeKeyFor=e=>{const r=e.stack?.split("\n")[1]?.trim();return r?`${e.message}::${r}`:e.message},capDedupeMap=()=>{for(;dedupeMap.size>200;){const e=dedupeMap.keys().next().value;if(void 0===e)return;dedupeMap.delete(e)}},shouldForward=e=>{if(loggerOptions.dedupeWindowMs<=0)return{forward:!0};const r=Date.now(),o=dedupeMap.get(e);if(!o||r-o.windowStart>=loggerOptions.dedupeWindowMs){const t=o?.suppressedCount??0;return dedupeMap.set(e,{windowStart:r,count:1,suppressedCount:0}),capDedupeMap(),t>0?{forward:!0,suppressedCount:t}:{forward:!0}}return o.count+=1,o.count>loggerOptions.dedupeMaxPerWindow?(o.suppressedCount+=1,{forward:!1}):{forward:!0}},buildErrorContext=(e,r)=>{const o={...deviceContext};return null!==currentScreen&&(o.screen=currentScreen),r&&(o.suppressedCount=r),{...o,...e}},forwardErrorToRemote=(e,r)=>{const o=dedupeKeyFor(e),{forward:t,suppressedCount:s}=shouldForward(o);t&&safe(()=>remote?.recordError?.(e,buildErrorContext(r,s)))},logDebug=(e,...r)=>{isEnabled()&&isLogLevelAllowed(LogLevels.DEBUG.level)&&console.debug(`${LogLevels.DEBUG.color}[DEBUG]: ${(0,_redact.redactString)(e)}${Colors.RESET}`,...(0,_redact.redact)(r))},logInfo=(e,...r)=>{const o=(0,_redact.redactString)(e),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed(LogLevels.INFO.level)&&(console.info(`${LogLevels.INFO.color}[INFO]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("info",o,t)))},logWarning=(e,...r)=>{const o=(0,_redact.redactString)(e),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed(LogLevels.WARN.level)&&(console.warn(`${LogLevels.WARN.color}[WARNING]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("warn",o,t)))},logError=(e,...r)=>{const o=(0,_redact.redactString)(e),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed(LogLevels.ERROR.level)&&console.error(`${LogLevels.ERROR.color}[ERROR]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("error",o,t));const s=t[0];void 0!==s&&forwardErrorToRemote(toErrorObject(s),{source:"Logger.error",message:o})},breadcrumb=(e,r)=>{const o=(0,_redact.redactString)(e),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed("info")&&console.info(`${Colors.BLUE}[BREADCRUMB]: ${o}${Colors.RESET}`,t??""),safe(()=>remote?.breadcrumb?.(o,t))},recordError=(e,r)=>{const o=(0,_redact.redact)(toErrorObject(e)),t=(0,_redact.redact)(r);isEnabled()&&isLogLevelAllowed("error")&&console.error(`${Colors.RED_BOLD}[RECORDED]: ${o.message}${Colors.RESET}`,t??""),forwardErrorToRemote(o,t)},asDict=e=>"object"==typeof e&&null!==e?e:null,headerValue=(e,r)=>{const o=asDict(e);if(!o)return null;const t="function"==typeof o.get?o.get.call(e,r):o[r]??o[r.toLowerCase()];return"string"==typeof t&&""!==t?t:null},requestFailed=(e,r={})=>{const{label:o,expectedStatuses:t=[],...s}=r,n=asDict(e),l=asDict(n?.config),c=asDict(n?.response),i=asDict(c?.data),d="number"==typeof c?.status?c.status:null,a="string"==typeof l?.method?l.method.toUpperCase():null,g="string"==typeof l?.url?l.url.split("?")[0]??null:null,u=o??([a,g].filter(Boolean).join(" ")||"Request");if("CanceledError"===n?.name||"ERR_CANCELED"===n?.code||!0===n?.__CANCEL__)return void logInfo(`${u} canceled`);if(null!==d&&t.includes(d))return void logInfo(`${u} refused (${d})`);const p="string"==typeof n?.message?n.message:e instanceof Error?e.message:null,E=(0,_redact.redactString)(null===d?`${u} failed`:`${u} failed (${d})`);isEnabled()&&isLogLevelAllowed(LogLevels.ERROR.level)&&console.error(`${LogLevels.ERROR.color}[ERROR]: ${E}${Colors.RESET}`,(0,_redact.redactString)(p??"")),safe(()=>remote?.log?.("error",E,[]));const f=(0,_index.getConfigValue)("correlationIdHeader",null),L=(0,_redact.redact)({source:"Logger.requestFailed",label:u,status:d,method:a,path:g,code:"string"==typeof i?.code?i.code:null,traceId:"string"==typeof i?.traceId?i.traceId:null,correlationId:f?headerValue(l?.headers,f):null,detail:p,...s});forwardErrorToRemote((0,_redact.redact)(new Error(E)),L)},setUser=(e,r)=>{safe(()=>remote?.setUser?.(e,r))},setAttribute=(e,r)=>{safe(()=>remote?.setAttribute?.(e,r))},trackScreen=(e,r)=>{e!==currentScreen&&(currentScreen=e,screenHistory.push(e),screenHistory.length>loggerOptions.screenHistoryLimit&&(screenHistory=screenHistory.slice(screenHistory.length-loggerOptions.screenHistoryLimit)),breadcrumb(`Screen: ${e}`,r))},getCurrentScreen=()=>currentScreen,getScreenHistory=()=>[...screenHistory],Logger=exports.Logger={debug:logDebug,info:logInfo,warn:logWarning,error:logError,breadcrumb:breadcrumb,recordError:recordError,requestFailed:requestFailed,setUser:setUser,setAttribute:setAttribute,trackScreen:trackScreen,getCurrentScreen:getCurrentScreen,getScreenHistory:getScreenHistory};
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"PUSH_STATUS",{enumerable:!0,get:function(){return _status.PUSH_STATUS}}),Object.defineProperty(exports,"disablePush",{enumerable:!0,get:function(){return _push.disablePush}}),Object.defineProperty(exports,"enablePush",{enumerable:!0,get:function(){return _push.enablePush}}),Object.defineProperty(exports,"initPush",{enumerable:!0,get:function(){return _push.initPush}}),Object.defineProperty(exports,"notifyLocal",{enumerable:!0,get:function(){return _push.notifyLocal}}),Object.defineProperty(exports,"onAction",{enumerable:!0,get:function(){return _push.onAction}}),Object.defineProperty(exports,"setBadgeCount",{enumerable:!0,get:function(){return _push.setBadgeCount}}),Object.defineProperty(exports,"setCategories",{enumerable:!0,get:function(){return _push.setCategories}}),Object.defineProperty(exports,"setPushImplementation",{enumerable:!0,get:function(){return _push.setPushImplementation}}),Object.defineProperty(exports,"usePush",{enumerable:!0,get:function(){return _push.usePush}});var _push=require("./push.js"),_status=require("./status.js");
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),Object.defineProperty(exports,"PUSH_STATUS",{enumerable:!0,get:function(){return _status.PUSH_STATUS}}),Object.defineProperty(exports,"disablePush",{enumerable:!0,get:function(){return _push.disablePush}}),Object.defineProperty(exports,"enablePush",{enumerable:!0,get:function(){return _push.enablePush}}),Object.defineProperty(exports,"initPush",{enumerable:!0,get:function(){return _push.initPush}}),Object.defineProperty(exports,"notifyLocal",{enumerable:!0,get:function(){return _push.notifyLocal}}),Object.defineProperty(exports,"onAction",{enumerable:!0,get:function(){return _push.onAction}}),Object.defineProperty(exports,"onReceive",{enumerable:!0,get:function(){return _push.onReceive}}),Object.defineProperty(exports,"resetPush",{enumerable:!0,get:function(){return _push.resetPush}}),Object.defineProperty(exports,"setBadgeCount",{enumerable:!0,get:function(){return _push.setBadgeCount}}),Object.defineProperty(exports,"setCategories",{enumerable:!0,get:function(){return _push.setCategories}}),Object.defineProperty(exports,"setPushImplementation",{enumerable:!0,get:function(){return _push.setPushImplementation}}),Object.defineProperty(exports,"usePush",{enumerable:!0,get:function(){return _push.usePush}});var _push=require("./push.js"),_status=require("./status.js");
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.usePush=exports.toPushAction=exports.toNotificationActions=exports.setPushImplementation=exports.setCategories=exports.setBadgeCount=exports.routeTaskPayload=exports.resolveModules=exports.onAction=exports.notifyLocal=exports.initPush=exports.enablePush=exports.disablePush=void 0;var _react=require("react"),_reactNative=require("react-native"),_index=require("../logger/index.js");let resolved,override=null;const setPushImplementation=e=>{override=e};exports.setPushImplementation=setPushImplementation;const resolveModules=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{if(require("expo").isRunningInExpoGo())return resolved=null,resolved;const e=require("expo-notifications"),t=require("expo-application");resolved={notifications:e,applicationId:t.applicationId}}catch(e){_index.Logger.warn("[push] expo-notifications is not available; push is off",e),resolved=null}return resolved};exports.resolveModules=resolveModules;let state={status:"unknown",enabled:!1};const subscribers=new Set,setState=e=>{state={...state,...e};for(const e of subscribers)e()};let config=null,channelsReady=Promise.resolve(),tokenSubscription=null,actionHandler=null,listening=!1;const pendingResponses=[],routedAt=new Map,REPEAT_WINDOW_MS=1e4,requireConfig=()=>{if(!config)throw new Error("[@webority/mobile-core] call initPush before enablePush or disablePush.");return config},toPermissionStatus=e=>e.granted?"granted":"denied"===e.status?"denied":"unknown",toStringData=e=>{const t={};for(const[n,o]of Object.entries(e??{}))null!=o&&(t[n]="object"==typeof o?JSON.stringify(o):String(o));return t},contentData=e=>{const t=e.notification.request.content;if(t.data)return toStringData(t.data);if("string"==typeof t.dataString)try{return toStringData(JSON.parse(t.dataString))}catch(e){_index.Logger.error("[push] notification data is not JSON",e)}return{}},triggerData=e=>{const t=e.notification.request.trigger,n={...t?.remoteMessage?.data??{}};for(const[e,o]of Object.entries(t?.payload??{}))"aps"===e||"body"===e&&"object"==typeof o||(n[e]=o);return toStringData(n)},responseData=e=>({...triggerData(e),...contentData(e)}),toPushAction=e=>({actionId:e.actionIdentifier,...e.userText?{userText:e.userText}:{},data:responseData(e),notificationId:e.notification.request.identifier});exports.toPushAction=toPushAction;const toNotificationActions=e=>e.map(e=>({identifier:e.id,buttonTitle:e.title,...e.textInput?{textInput:{submitButtonTitle:e.textInput.submitTitle,placeholder:e.textInput.placeholder}}:{},options:{opensAppToForeground:e.opensApp??!0,isDestructive:e.destructive??!1,isAuthenticationRequired:e.authenticationRequired??!1}}));exports.toNotificationActions=toNotificationActions;const routeResponse=async(e,t)=>{const n=`${t.notification.request.identifier}:${t.actionIdentifier}`,o=routedAt.get(n);if(void 0!==o&&Date.now()-o<1e4)return;if(t.actionIdentifier!==e.DEFAULT_ACTION_IDENTIFIER&&actionHandler){routedAt.set(n,Date.now());try{await actionHandler(toPushAction(t))}catch(e){_index.Logger.error("[push] onAction threw",e)}}else if(config){routedAt.set(n,Date.now());try{config.onOpen(responseData(t))}catch(e){_index.Logger.error("[push] onOpen threw",e)}}else pendingResponses.push(t)},drainPending=async e=>{const t=pendingResponses.splice(0);for(const n of t)await routeResponse(e,n)},listenForResponses=e=>{listening||(listening=!0,e.addNotificationResponseReceivedListener(t=>{routeResponse(e,t)}),e.getLastNotificationResponseAsync().then(async t=>(t&&await routeResponse(e,t),e.clearLastNotificationResponseAsync?.())).catch(e=>_index.Logger.error("[push] reading the launch notification failed",e)))},routeTaskPayload=async e=>{const t=resolveModules(),n=e;t&&"string"==typeof n?.actionIdentifier&&"string"==typeof n.notification?.request?.identifier&&await routeResponse(t.notifications,n)};exports.routeTaskPayload=routeTaskPayload;const createChannels=async(e,t)=>{for(const n of t)try{await e.setNotificationChannelAsync(n.id,{name:n.name,importance:"high"===n.importance?e.AndroidImportance.HIGH:e.AndroidImportance.DEFAULT})}catch(e){_index.Logger.error(`[push] creating channel ${n.id} failed`,e)}},toDevice=async(e,t)=>{const n="android"===e.type?"Fcm":"ios"===e.type?"Apns":null;if(!n||"string"!=typeof e.data)return null;if(!t)throw new Error("[@webority/mobile-core] push needs the app bundle id from expo-application.");return{installationId:await requireConfig().getInstallationId(),platform:n,token:e.data,appId:t}},initPush=e=>{const t=null===config;if(config=e,!t)return;const n=resolveModules();if(!n)return void setState({status:"unavailable"});const o=n.notifications;o.setNotificationHandler({handleNotification:async()=>{const e=config?.showInForeground?.()??!0;return{shouldShowBanner:e,shouldShowList:e,shouldPlaySound:e&&(config?.playSoundInForeground?.()??!0),shouldSetBadge:!0}}}),"android"===_reactNative.Platform.OS&&e.androidChannels?.length&&(channelsReady=createChannels(o,e.androidChannels)),listenForResponses(o),drainPending(o),o.getPermissionsAsync().then(e=>setState({status:toPermissionStatus(e)})).catch(e=>_index.Logger.error("[push] permission check failed",e))};exports.initPush=initPush;let registering=null,registeredKey=null;const registerOnce=e=>{const t=JSON.stringify(e);if(registering?.key===t)return registering.promise;const n=requireConfig().register(e).then(()=>{registeredKey=t}).finally(()=>{registering?.promise===n&&(registering=null)});return registering={key:t,promise:n},n},enablePush=async()=>{requireConfig();const e=resolveModules();if(!e)return setState({status:"unavailable"}),"unavailable";const t=e.notifications;await channelsReady;let n=await t.getPermissionsAsync();if(n.granted||(n=await t.requestPermissionsAsync()),!n.granted)return setState({status:"denied"}),"denied";const o=await toDevice(await t.getDevicePushTokenAsync(),e.applicationId);return o?(await registerOnce(o),tokenSubscription?.remove(),tokenSubscription=t.addPushTokenListener(t=>{toDevice(t,e.applicationId).then(e=>e&&JSON.stringify(e)!==registeredKey?registerOnce(e):void 0).catch(e=>_index.Logger.error("[push] re-registering a new token failed",e))}),setState({status:"granted",enabled:!0}),"granted"):(setState({status:"unavailable"}),"unavailable")};exports.enablePush=enablePush;const disablePush=async()=>{const e=requireConfig();tokenSubscription?.remove(),tokenSubscription=null,registeredKey=null,setState({enabled:!1}),await e.unregister(await e.getInstallationId())};exports.disablePush=disablePush;const setBadgeCount=async e=>{const t=resolveModules();t&&await t.notifications.setBadgeCountAsync(e)};exports.setBadgeCount=setBadgeCount;const setCategories=async e=>{const t=resolveModules();if(t)for(const n of e)await t.notifications.setNotificationCategoryAsync(n.id,toNotificationActions(n.actions))};exports.setCategories=setCategories;const onAction=e=>{actionHandler=e;const t=resolveModules();return t&&(listenForResponses(t.notifications),drainPending(t.notifications)),()=>{actionHandler===e&&(actionHandler=null)}};exports.onAction=onAction;const notifyLocal=async({title:e,body:t,data:n})=>{const o=resolveModules();o&&await o.notifications.scheduleNotificationAsync({content:{title:e,body:t,data:n??{}},trigger:null})};exports.notifyLocal=notifyLocal;const subscribe=e=>(subscribers.add(e),()=>{subscribers.delete(e)}),getSnapshot=()=>state,enable=async()=>{await enablePush()},usePush=()=>({...(0,_react.useSyncExternalStore)(subscribe,getSnapshot,getSnapshot),enable:enable,disable:disablePush});exports.usePush=usePush;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.usePush=exports.toPushReceived=exports.toPushAction=exports.toNotificationActions=exports.setPushImplementation=exports.setCategories=exports.setBadgeCount=exports.routeTaskPayload=exports.resolveModules=exports.resetPush=exports.onReceive=exports.onAction=exports.notifyLocal=exports.initPush=exports.enablePush=exports.disablePush=void 0;var _react=require("react"),_reactNative=require("react-native"),_index=require("../logger/index.js");let resolved,override=null;const setPushImplementation=e=>{override=e};exports.setPushImplementation=setPushImplementation;const resolveModules=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{if(require("expo").isRunningInExpoGo())return resolved=null,resolved;const e=require("expo-notifications"),t=require("expo-application");resolved={notifications:e,applicationId:t.applicationId,getApnsEnvironment:()=>t.getIosPushNotificationServiceEnvironmentAsync()}}catch(e){_index.Logger.warn("[push] expo-notifications is not available; push is off",e),resolved=null}return resolved};exports.resolveModules=resolveModules;let state={status:"unknown",enabled:!1};const subscribers=new Set,setState=e=>{state={...state,...e};for(const e of subscribers)e()};let config=null,channelsReady=Promise.resolve(),tokenSubscription=null,actionHandler=null;const receiveHandlers=new Set;let listening=!1,listeningForReceived=!1;const pendingResponses=[],routedAt=new Map,REPEAT_WINDOW_MS=1e4,requireConfig=()=>{if(!config)throw new Error("[@webority/mobile-core] call initPush before enablePush or disablePush.");return config},toPermissionStatus=e=>e.granted?"granted":"denied"===e.status?"denied":"unknown",toStringData=e=>{const t={};for(const[n,o]of Object.entries(e??{}))null!=o&&(t[n]="object"==typeof o?JSON.stringify(o):String(o));return t},contentData=e=>{const t=e.request.content;if(t.data)return toStringData(t.data);if("string"==typeof t.dataString)try{return toStringData(JSON.parse(t.dataString))}catch(e){_index.Logger.error("[push] notification data is not JSON",e)}return{}},triggerData=e=>{const t=e.request.trigger,n={...t?.remoteMessage?.data??{}};for(const[e,o]of Object.entries(t?.payload??{}))"aps"===e||"body"===e&&"object"==typeof o||(n[e]=o);return toStringData(n)},notificationData=e=>({...triggerData(e),...contentData(e)}),responseData=e=>notificationData(e.notification),toPushReceived=e=>({notificationId:e.request.identifier,title:e.request.content.title??null,body:e.request.content.body??null,data:notificationData(e)});exports.toPushReceived=toPushReceived;const toPushAction=e=>({actionId:e.actionIdentifier,...e.userText?{userText:e.userText}:{},data:responseData(e),notificationId:e.notification.request.identifier});exports.toPushAction=toPushAction;const toNotificationActions=e=>e.map(e=>({identifier:e.id,buttonTitle:e.title,...e.textInput?{textInput:{submitButtonTitle:e.textInput.submitTitle,placeholder:e.textInput.placeholder}}:{},options:{opensAppToForeground:e.opensApp??!0,isDestructive:e.destructive??!1,isAuthenticationRequired:e.authenticationRequired??!1}}));exports.toNotificationActions=toNotificationActions;const routeResponse=async(e,t)=>{const n=`${t.notification.request.identifier}:${t.actionIdentifier}`,o=routedAt.get(n);if(void 0!==o&&Date.now()-o<1e4)return;if(t.actionIdentifier!==e.DEFAULT_ACTION_IDENTIFIER&&actionHandler){routedAt.set(n,Date.now());try{await actionHandler(toPushAction(t))}catch(e){_index.Logger.error("[push] onAction threw",e)}}else if(config){routedAt.set(n,Date.now());try{config.onOpen(responseData(t))}catch(e){_index.Logger.error("[push] onOpen threw",e)}}else pendingResponses.push(t)},drainPending=async e=>{const t=pendingResponses.splice(0);for(const n of t)await routeResponse(e,n)},listenForResponses=e=>{listening||(listening=!0,e.addNotificationResponseReceivedListener(t=>{routeResponse(e,t)}),e.getLastNotificationResponseAsync().then(async t=>(t&&await routeResponse(e,t),e.clearLastNotificationResponseAsync?.())).catch(e=>_index.Logger.error("[push] reading the launch notification failed",e)))},routeTaskPayload=async e=>{const t=resolveModules(),n=e;t&&"string"==typeof n?.actionIdentifier&&"string"==typeof n.notification?.request?.identifier&&await routeResponse(t.notifications,n)};exports.routeTaskPayload=routeTaskPayload;const createChannels=async(e,t)=>{for(const n of t)try{await e.setNotificationChannelAsync(n.id,{name:n.name,importance:"high"===n.importance?e.AndroidImportance.HIGH:e.AndroidImportance.DEFAULT})}catch(e){_index.Logger.error(`[push] creating channel ${n.id} failed`,e)}},APNS_ENVIRONMENTS={development:"Development",production:"Production"},readApnsEnvironment=async e=>{try{return APNS_ENVIRONMENTS[await e.getApnsEnvironment()??""]??null}catch(e){return _index.Logger.warn("[push] reading the APNs environment failed",e),null}},toDevice=async(e,t)=>{const n="android"===e.type?"Fcm":"ios"===e.type?"Apns":null;if(!n||"string"!=typeof e.data)return null;if(!t.applicationId)throw new Error("[@webority/mobile-core] push needs the app bundle id from expo-application.");return{installationId:await requireConfig().getInstallationId(),platform:n,token:e.data,appId:t.applicationId,apnsEnvironment:"Apns"===n?await readApnsEnvironment(t):null}},initPush=e=>{const t=null===config;if(config=e,!t)return;const n=resolveModules();if(!n)return void setState({status:"unavailable"});const o=n.notifications;o.setNotificationHandler({handleNotification:async()=>{const e=config?.showInForeground?.()??!0;return{shouldShowBanner:e,shouldShowList:e,shouldPlaySound:e&&(config?.playSoundInForeground?.()??!0),shouldSetBadge:!0}}}),"android"===_reactNative.Platform.OS&&e.androidChannels?.length&&(channelsReady=createChannels(o,e.androidChannels)),listenForResponses(o),drainPending(o),o.getPermissionsAsync().then(e=>setState({status:toPermissionStatus(e)})).catch(e=>_index.Logger.error("[push] permission check failed",e))};exports.initPush=initPush;let registering=null,registeredKey=null,unregistering=null,tokenDrop=Promise.resolve();const registerOnce=e=>{const t=JSON.stringify(e);if(registering?.key===t)return registering.promise;const n=requireConfig().register(e).then(()=>{registeredKey=t}).finally(()=>{registering?.promise===n&&(registering=null)});return registering={key:t,promise:n},n},enablePush=async({prompt:e=!0}={})=>{requireConfig();const t=resolveModules();if(!t)return setState({status:"unavailable"}),"unavailable";const n=t.notifications;await channelsReady,await tokenDrop;let o=await n.getPermissionsAsync();if(!o.granted&&e&&(o=await n.requestPermissionsAsync()),!o.granted){const e=toPermissionStatus(o);return setState({status:e}),e}const i=await toDevice(await n.getDevicePushTokenAsync(),t);return i?(await registerOnce(i),tokenSubscription?.remove(),tokenSubscription=n.addPushTokenListener(e=>{(unregistering??Promise.resolve()).then(()=>state.enabled?toDevice(e,t):null).then(e=>e&&JSON.stringify(e)!==registeredKey?registerOnce(e):void 0).catch(e=>_index.Logger.error("[push] re-registering a new token failed",e))}),setState({status:"granted",enabled:!0}),"granted"):(setState({status:"unavailable"}),"unavailable")};exports.enablePush=enablePush;const stopFollowingToken=()=>{tokenSubscription?.remove(),tokenSubscription=null,registeredKey=null,setState({enabled:!1})},disablePush=async()=>{const e=requireConfig(),t=await e.getInstallationId();await(registering?.promise.catch(()=>{}));let settle=()=>{};const n=new Promise(e=>{settle=e});unregistering=n;try{await e.unregister(t),stopFollowingToken()}finally{unregistering===n&&(unregistering=null),settle()}};exports.disablePush=disablePush;const resetPush=async()=>{stopFollowingToken();const e=resolveModules();if(!e)return;const t=e.notifications.unregisterForNotificationsAsync();tokenDrop=t.catch(()=>{}),await t};exports.resetPush=resetPush;const setBadgeCount=async e=>{const t=resolveModules();t&&await t.notifications.setBadgeCountAsync(e)};exports.setBadgeCount=setBadgeCount;const setCategories=async e=>{const t=resolveModules();if(t)for(const n of e)await t.notifications.setNotificationCategoryAsync(n.id,toNotificationActions(n.actions))};exports.setCategories=setCategories;const onAction=e=>{actionHandler=e;const t=resolveModules();return t&&(listenForResponses(t.notifications),drainPending(t.notifications)),()=>{actionHandler===e&&(actionHandler=null)}};exports.onAction=onAction;const onReceive=e=>{receiveHandlers.add(e);const t=resolveModules();return t&&!listeningForReceived&&(listeningForReceived=!0,t.notifications.addNotificationReceivedListener(e=>{const t=toPushReceived(e);for(const e of[...receiveHandlers])try{e(t)}catch(e){_index.Logger.error("[push] onReceive threw",e)}})),()=>{receiveHandlers.delete(e)}};exports.onReceive=onReceive;const notifyLocal=async({title:e,body:t,data:n})=>{const o=resolveModules();o&&await o.notifications.scheduleNotificationAsync({content:{title:e,body:t,data:n??{}},trigger:null})};exports.notifyLocal=notifyLocal;const subscribe=e=>(subscribers.add(e),()=>{subscribers.delete(e)}),getSnapshot=()=>state,enable=async()=>{await enablePush()},usePush=()=>({...(0,_react.useSyncExternalStore)(subscribe,getSnapshot,getSnapshot),enable:enable,disable:disablePush});exports.usePush=usePush;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.whereIn=exports.toSqliteValue=exports.setSqliteImplementation=exports.openDatabase=exports.isSqliteAvailable=exports.fromSqliteBoolean=exports.assertSqlIdentifier=exports.MIGRATIONS_TABLE=exports.DEFAULT_PRAGMAS=void 0;var ExpoSqlite=_interopRequireWildcard(require("expo-sqlite")),_index=require("../logger/index.js"),_expo=require("./adapters/expo.js");function _interopRequireWildcard(e,t){if("function"==typeof WeakMap)var r=new WeakMap,n=new WeakMap;return(_interopRequireWildcard=function(e,t){if(!t&&e&&e.__esModule)return e;var i,o,a={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return a;if(i=t?n:r){if(i.has(e))return i.get(e);i.set(e,a)}for(const t in e)"default"!==t&&{}.hasOwnProperty.call(e,t)&&((o=(i=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,t))&&(o.get||o.set)?i(a,t,o):a[t]=e[t]);return a})(e,t)}let expoDriver,backend=null;const setSqliteImplementation=e=>{backend=e};exports.setSqliteImplementation=setSqliteImplementation;const hasNativeSqlite=e=>"function"==typeof e?.openDatabaseAsync,resolveDriver=()=>{if(backend)return backend;if(void 0===expoDriver){const e=ExpoSqlite;expoDriver=hasNativeSqlite(e)?(0,_expo.adaptExpoSqlite)(e):null}return expoDriver},isSqliteAvailable=()=>null!==resolveDriver();exports.isSqliteAvailable=isSqliteAvailable;const requireBackend=e=>{const t=resolveDriver();if(!t){const t=new Error(`[@webority/mobile-core] No SQLite driver available; cannot ${e}. Install expo-sqlite (the only sanctioned driver) in the consuming app.`);throw _index.Logger.error(t.message),t}return t},MIGRATIONS_TABLE=exports.MIGRATIONS_TABLE="_webority_migrations",IDENTIFIER=/^[A-Za-z_][A-Za-z0-9_]*$/,assertSqlIdentifier=(e,t="identifier")=>{if("string"!=typeof e||!IDENTIFIER.test(e))throw new Error(`[@webority/mobile-core] Invalid SQL ${t} ${JSON.stringify(e)}. Only letters, digits and underscores are allowed, and it must not start with a digit. Pass values as bound parameters instead of building them into the name.`);return e};exports.assertSqlIdentifier=assertSqlIdentifier;const quoteIdentifier=(e,t="identifier")=>`"${assertSqlIdentifier(e,t)}"`,escapeIdentifier=e=>`"${e.replace(/"/g,'""')}"`,COLUMN_DEFINITION=/^[A-Za-z0-9_ ,.()'"+*/=<>|%:$-]+$/,assertColumnDefinition=(e,t)=>{const r="string"==typeof e?e.trim():"";if(0===r.length||!COLUMN_DEFINITION.test(r)||r.includes("--"))throw new Error(`[@webority/mobile-core] Invalid column definition for "${t}": ${JSON.stringify(e)}. A definition is raw SQL such as 'INTEGER NOT NULL DEFAULT 0' and may not contain a statement separator or a comment.`);return r},toSqliteValue=e=>void 0===e?null:"boolean"==typeof e?e?1:0:e instanceof Date?e.toISOString():e;exports.toSqliteValue=toSqliteValue;const fromSqliteBoolean=e=>{if("boolean"==typeof e)return e;if("number"==typeof e)return 0!==e;if("string"==typeof e){const t=e.trim().toLowerCase();return"1"===t||"true"===t}return!1};exports.fromSqliteBoolean=fromSqliteBoolean;const whereIn=(e,t)=>{const r=quoteIdentifier(e,"column name");return 0===t.length?{sql:"1 = 0",params:[]}:{sql:`${r} IN (${t.map(()=>"?").join(", ")})`,params:t.map(toSqliteValue)}};exports.whereIn=whereIn;const isRawWhere=e=>"string"==typeof e.sql&&Object.keys(e).every(e=>"sql"===e||"params"===e),buildWhere=e=>{if(!e)return{clause:"",params:[]};if(isRawWhere(e)){const t=e.sql.trim();return 0===t.length?{clause:"",params:[]}:{clause:` WHERE ${t}`,params:[...e.params??[]]}}const t=Object.entries(e);if(0===t.length)return{clause:"",params:[]};const r=[],n=[];for(const[e,i]of t){const t=quoteIdentifier(e,"column name"),o=toSqliteValue(i);null===o?r.push(`${t} IS NULL`):(r.push(`${t} = ?`),n.push(o))}return{clause:` WHERE ${r.join(" AND ")}`,params:n}},buildOrderBy=e=>{if(void 0===e)return"";const t=e.split(",").map(e=>e.trim()).filter(e=>e.length>0);if(0===t.length)return"";return` ORDER BY ${t.map(t=>{const[r,n,...i]=t.split(/\s+/);if(i.length>0)throw new Error(`[@webority/mobile-core] Invalid orderBy term ${JSON.stringify(t)}. Expected a column name optionally followed by ASC or DESC.`);const o=quoteIdentifier(r,"orderBy column");if(void 0===n)return o;const a=n.toUpperCase();if("ASC"!==a&&"DESC"!==a)throw new Error(`[@webority/mobile-core] Invalid sort direction ${JSON.stringify(n)} in orderBy ${JSON.stringify(e)}. Expected ASC or DESC.`);return`${o} ${a}`}).join(", ")}`},assertNonNegativeInteger=(e,t)=>{if(!Number.isInteger(e)||e<0)throw new Error(`[@webority/mobile-core] Invalid ${t} ${JSON.stringify(e)}. Expected a non-negative integer.`);return e},hashString=e=>{let t=2166136261;for(let r=0;r<e.length;r+=1)t^=e.charCodeAt(r),t=Math.imul(t,16777619)>>>0;return`${t.toString(16).padStart(8,"0")}-${e.length.toString(16)}`},migrationChecksum=e=>{if("function"==typeof e.up)return hashString("fn");const t=Array.isArray(e.up)?e.up.join("\n"):e.up;return hashString(t)},DEFAULT_PRAGMAS=exports.DEFAULT_PRAGMAS={journal_mode:"WAL",synchronous:"NORMAL",foreign_keys:"ON",busy_timeout:5e3},PRAGMA_VALUE=/^[A-Za-z0-9_-]+$/,renderPragmaValue=(e,t)=>{if("number"==typeof t){if(!Number.isFinite(t))throw new Error(`[@webority/mobile-core] Invalid value for PRAGMA ${e}: ${t}.`);return String(t)}if(!PRAGMA_VALUE.test(t))throw new Error(`[@webority/mobile-core] Invalid value ${JSON.stringify(t)} for PRAGMA ${e}. PRAGMA values cannot be bound, so only letters, digits, underscores and hyphens are accepted.`);return t},createDb=(e,t)=>{let r=Promise.resolve();const execute=async(e,r)=>{try{return await t.execute(e,r)}catch(t){throw _index.Logger.error(`[@webority/mobile-core] SQLite execute failed: ${e}`,t),t}},select=async(e,t)=>(await execute(e,t)).rows,selectOne=async(e,t)=>{const r=await select(e,t);return r.length>0?r[0]:null},runTransaction=async e=>{let r;await t.execute("BEGIN");try{r=await e({execute:execute,select:select,selectOne:selectOne})}catch(e){try{await t.execute("ROLLBACK")}catch(e){_index.Logger.error("[@webority/mobile-core] SQLite ROLLBACK failed",e)}throw e}try{await t.execute("COMMIT")}catch(e){try{await t.execute("ROLLBACK")}catch{}throw e}return r},transaction=e=>{const t=r.then(()=>runTransaction(e),()=>runTransaction(e));return r=t.catch(()=>{}),t},insertSql=(e,t)=>{const r=quoteIdentifier(e,"table name"),n=Object.entries(t);if(0===n.length)throw new Error(`[@webority/mobile-core] Cannot insert an empty row into ${e}. Pass at least one column.`);const i=n.map(([e])=>quoteIdentifier(e,"column name")),o=n.map(()=>"?");return{sql:`INSERT INTO ${r} (${i.join(", ")}) VALUES (${o.join(", ")})`,params:n.map(([,e])=>toSqliteValue(e))}},findAll=async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=buildWhere(t),o=[...i.params];let a=`SELECT * FROM ${n}${i.clause}${buildOrderBy(r?.orderBy)}`;return void 0!==r?.limit?(assertNonNegativeInteger(r.limit,"limit"),a+=" LIMIT ?",o.push(r.limit)):void 0!==r?.offset&&(a+=" LIMIT -1"),void 0!==r?.offset&&(assertNonNegativeInteger(r.offset,"offset"),a+=" OFFSET ?",o.push(r.offset)),select(a,o)},ensureLedger=async()=>{await execute(`CREATE TABLE IF NOT EXISTS ${escapeIdentifier(MIGRATIONS_TABLE)} (version INTEGER PRIMARY KEY, name TEXT, applied_at TEXT NOT NULL, checksum TEXT NOT NULL)`)},n=`INSERT OR REPLACE INTO ${escapeIdentifier(MIGRATIONS_TABLE)} (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)`,wipe=async()=>{const e=await selectOne("PRAGMA foreign_keys"),t=1===Number(e?.foreign_keys??0);await execute("PRAGMA foreign_keys = OFF");try{await transaction(async e=>{const t=await e.select("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name <> ?",[MIGRATIONS_TABLE]);for(const r of t)await e.execute(`DELETE FROM ${escapeIdentifier(r.name)}`)})}finally{await execute("PRAGMA foreign_keys = "+(t?"ON":"OFF"))}},migrate=async(t,r)=>{const i=new Set;for(const r of t){if(i.has(r.version))throw new Error(`[@webority/mobile-core] Duplicate migration version ${r.version} for database "${e}".`);i.add(r.version)}const o=await selectOne("PRAGMA user_version");let a=Number(o?.user_version??0);if(t.length>0&&!r?.allowDowngrade){const r=t.reduce((e,t)=>Math.max(e,t.version),0);if(a>r)throw new Error(`[@webority/mobile-core] Database "${e}" is at schema version ${a}, which is newer than the highest migration supplied (${r}). This build would run old code against a newer schema. Ship the missing migrations, or pass { allowDowngrade: true } if this build is known to be compatible.`)}await ensureLedger();const s=await select(`SELECT version, checksum FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),l=new Map(s.map(e=>[Number(e.version),String(e.checksum)]));for(const r of t){if(r.version>a)continue;const t=l.get(r.version);if(void 0===t)continue;const n=migrationChecksum(r);if(t!==n)throw new Error(`[@webority/mobile-core] Migration ${r.version} has changed since it was applied to database "${e}" (recorded ${t}, now ${n}). A released migration must never be edited; add a new one instead.`)}const c=t.filter(e=>e.version>a).sort((e,t)=>e.version-t.version);for(const e of c){const t=migrationChecksum(e);await transaction(async r=>{if("function"==typeof e.up)await e.up(r);else{const t=Array.isArray(e.up)?e.up:[e.up];for(const e of t)await r.execute(e)}await r.execute(n,[e.version,e.name??null,(new Date).toISOString(),t]),await r.execute(`PRAGMA user_version = ${e.version}`)}),a=e.version}return a};return{name:e,execute:execute,select:select,selectOne:selectOne,transaction:transaction,insert:async(e,t)=>{const{sql:r,params:n}=insertSql(e,t);return(await execute(r,n)).insertId},insertMany:async(e,t)=>{if(0===t.length)return 0;const r=t.map(t=>insertSql(e,t));return await transaction(async e=>{for(const t of r)await e.execute(t.sql,t.params)}),t.length},update:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=Object.entries(t);if(0===i.length)throw new Error(`[@webority/mobile-core] Cannot update ${e} with no values. Pass at least one column to set.`);const o=i.map(([e])=>`${quoteIdentifier(e,"column name")} = ?`),a=i.map(([,e])=>toSqliteValue(e)),s=buildWhere(r);return(await execute(`UPDATE ${n} SET ${o.join(", ")}${s.clause}`,[...a,...s.params])).rowsAffected},remove:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t);return(await execute(`DELETE FROM ${r}${n.clause}`,n.params)).rowsAffected},upsert:async(e,t,r)=>{if(0===r.length)throw new Error(`[@webority/mobile-core] upsert on ${e} needs at least one conflict column; without one SQLite has no key to match an existing row on.`);const{sql:n,params:i}=insertSql(e,t),o=new Set(r),a=r.map(e=>quoteIdentifier(e,"column name")),s=Object.keys(t).filter(e=>!o.has(e)).map(e=>{const t=quoteIdentifier(e,"column name");return`${t} = excluded.${t}`}),l=s.length>0?`DO UPDATE SET ${s.join(", ")}`:"DO NOTHING";await execute(`${n} ON CONFLICT (${a.join(", ")}) ${l}`,i)},findAll:findAll,findOne:async(e,t)=>{const r=await findAll(e,t,{limit:1});return r.length>0?r[0]:null},count:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t),i=await selectOne(`SELECT COUNT(*) AS cnt FROM ${r}${n.clause}`,n.params);return Number(i?.cnt??0)},tableExists:async e=>{assertSqlIdentifier(e,"table name");return null!==await selectOne("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",[e])},createTable:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=Object.entries(t);if(0===i.length)throw new Error(`[@webority/mobile-core] Cannot create table ${e} with no columns.`);const o=i.map(([e,t])=>`${quoteIdentifier(e,"column name")} ${assertColumnDefinition(t,e)}`),a=!1===r?.ifNotExists?"":"IF NOT EXISTS ";await execute(`CREATE TABLE ${a}${n} (${o.join(", ")})`)},dropTable:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=!1===t?.ifExists?"":"IF EXISTS ";await execute(`DROP TABLE ${n}${r}`)},wipe:wipe,reset:async e=>(await wipe(),await ensureLedger(),await transaction(async e=>{await e.execute(`DELETE FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),await e.execute("PRAGMA user_version = 0")}),migrate(e)),migrate:migrate,baseline:async(e,t)=>{assertNonNegativeInteger(e,"baseline version"),await ensureLedger();const r=(new Date).toISOString();await transaction(async i=>{for(const o of t??[])o.version>e||await i.execute(n,[o.version,o.name??null,r,migrationChecksum(o)]);await i.execute(`PRAGMA user_version = ${e}`)})},close:async()=>{await t.close()}}},openDatabase=async(e,t)=>{const r=requireBackend("open"),n=await r.open(e,t?{location:t.location}:void 0),i=createDb(e,n);if(!1!==t?.pragmas){const e={...DEFAULT_PRAGMAS,...t?.pragmas??{}};for(const[t,r]of Object.entries(e))assertSqlIdentifier(t,"PRAGMA name"),await i.execute(`PRAGMA ${t} = ${renderPragmaValue(t,r)}`)}return i};exports.openDatabase=openDatabase;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.whereIn=exports.toSqliteValue=exports.setSqliteImplementation=exports.openDatabase=exports.isSqliteAvailable=exports.fromSqliteBoolean=exports.assertSqlIdentifier=exports.MIGRATIONS_TABLE=exports.DEFAULT_PRAGMAS=void 0;var ExpoSqlite=_interopRequireWildcard(require("expo-sqlite")),_index=require("../logger/index.js"),_expo=require("./adapters/expo.js");function _interopRequireWildcard(e,t){if("function"==typeof WeakMap)var r=new WeakMap,n=new WeakMap;return(_interopRequireWildcard=function(e,t){if(!t&&e&&e.__esModule)return e;var i,o,a={__proto__:null,default:e};if(null===e||"object"!=typeof e&&"function"!=typeof e)return a;if(i=t?n:r){if(i.has(e))return i.get(e);i.set(e,a)}for(const t in e)"default"!==t&&{}.hasOwnProperty.call(e,t)&&((o=(i=Object.defineProperty)&&Object.getOwnPropertyDescriptor(e,t))&&(o.get||o.set)?i(a,t,o):a[t]=e[t]);return a})(e,t)}let expoDriver,backend=null;const setSqliteImplementation=e=>{backend=e};exports.setSqliteImplementation=setSqliteImplementation;const hasNativeSqlite=e=>"function"==typeof e?.openDatabaseAsync,resolveDriver=()=>{if(backend)return backend;if(void 0===expoDriver){const e=ExpoSqlite;expoDriver=hasNativeSqlite(e)?(0,_expo.adaptExpoSqlite)(e):null}return expoDriver},isSqliteAvailable=()=>null!==resolveDriver();exports.isSqliteAvailable=isSqliteAvailable;const requireBackend=e=>{const t=resolveDriver();if(!t){const t=new Error(`[@webority/mobile-core] No SQLite driver available; cannot ${e}. Install expo-sqlite (the only sanctioned driver) in the consuming app.`);throw _index.Logger.error(t.message),t}return t},MIGRATIONS_TABLE=exports.MIGRATIONS_TABLE="_webority_migrations",IDENTIFIER=/^[A-Za-z_][A-Za-z0-9_]*$/,assertSqlIdentifier=(e,t="identifier")=>{if("string"!=typeof e||!IDENTIFIER.test(e))throw new Error(`[@webority/mobile-core] Invalid SQL ${t} ${JSON.stringify(e)}. Only letters, digits and underscores are allowed, and it must not start with a digit. Pass values as bound parameters instead of building them into the name.`);return e};exports.assertSqlIdentifier=assertSqlIdentifier;const quoteIdentifier=(e,t="identifier")=>`"${assertSqlIdentifier(e,t)}"`,escapeIdentifier=e=>`"${e.replace(/"/g,'""')}"`,COLUMN_DEFINITION=/^[A-Za-z0-9_ ,.()'"+*/=<>|%:$-]+$/,assertColumnDefinition=(e,t)=>{const r="string"==typeof e?e.trim():"";if(0===r.length||!COLUMN_DEFINITION.test(r)||r.includes("--"))throw new Error(`[@webority/mobile-core] Invalid column definition for "${t}": ${JSON.stringify(e)}. A definition is raw SQL such as 'INTEGER NOT NULL DEFAULT 0' and may not contain a statement separator or a comment.`);return r},toSqliteValue=e=>void 0===e?null:"boolean"==typeof e?e?1:0:e instanceof Date?e.toISOString():e;exports.toSqliteValue=toSqliteValue;const fromSqliteBoolean=e=>{if("boolean"==typeof e)return e;if("number"==typeof e)return 0!==e;if("string"==typeof e){const t=e.trim().toLowerCase();return"1"===t||"true"===t}return!1};exports.fromSqliteBoolean=fromSqliteBoolean;const whereIn=(e,t)=>{const r=quoteIdentifier(e,"column name");return 0===t.length?{sql:"1 = 0",params:[]}:{sql:`${r} IN (${t.map(()=>"?").join(", ")})`,params:t.map(toSqliteValue)}};exports.whereIn=whereIn;const isRawWhere=e=>"string"==typeof e.sql&&Object.keys(e).every(e=>"sql"===e||"params"===e),buildWhere=e=>{if(!e)return{clause:"",params:[]};if(isRawWhere(e)){const t=e.sql.trim();return 0===t.length?{clause:"",params:[]}:{clause:` WHERE ${t}`,params:[...e.params??[]]}}const t=Object.entries(e);if(0===t.length)return{clause:"",params:[]};const r=[],n=[];for(const[e,i]of t){const t=quoteIdentifier(e,"column name"),o=toSqliteValue(i);null===o?r.push(`${t} IS NULL`):(r.push(`${t} = ?`),n.push(o))}return{clause:` WHERE ${r.join(" AND ")}`,params:n}},buildOrderBy=e=>{if(void 0===e)return"";const t=e.split(",").map(e=>e.trim()).filter(e=>e.length>0);if(0===t.length)return"";return` ORDER BY ${t.map(t=>{const[r,n,...i]=t.split(/\s+/);if(i.length>0)throw new Error(`[@webority/mobile-core] Invalid orderBy term ${JSON.stringify(t)}. Expected a column name optionally followed by ASC or DESC.`);const o=quoteIdentifier(r,"orderBy column");if(void 0===n)return o;const a=n.toUpperCase();if("ASC"!==a&&"DESC"!==a)throw new Error(`[@webority/mobile-core] Invalid sort direction ${JSON.stringify(n)} in orderBy ${JSON.stringify(e)}. Expected ASC or DESC.`);return`${o} ${a}`}).join(", ")}`},assertNonNegativeInteger=(e,t)=>{if(!Number.isInteger(e)||e<0)throw new Error(`[@webority/mobile-core] Invalid ${t} ${JSON.stringify(e)}. Expected a non-negative integer.`);return e},hashString=e=>{let t=2166136261;for(let r=0;r<e.length;r+=1)t^=e.charCodeAt(r),t=Math.imul(t,16777619)>>>0;return`${t.toString(16).padStart(8,"0")}-${e.length.toString(16)}`},migrationChecksum=e=>{if("function"==typeof e.up)return hashString("fn");const t=Array.isArray(e.up)?e.up.join("\n"):e.up;return hashString(t)},DEFAULT_PRAGMAS=exports.DEFAULT_PRAGMAS={journal_mode:"WAL",synchronous:"NORMAL",foreign_keys:"ON",busy_timeout:5e3},PRAGMA_VALUE=/^[A-Za-z0-9_-]+$/,renderPragmaValue=(e,t)=>{if("number"==typeof t){if(!Number.isFinite(t))throw new Error(`[@webority/mobile-core] Invalid value for PRAGMA ${e}: ${t}.`);return String(t)}if(!PRAGMA_VALUE.test(t))throw new Error(`[@webority/mobile-core] Invalid value ${JSON.stringify(t)} for PRAGMA ${e}. PRAGMA values cannot be bound, so only letters, digits, underscores and hyphens are accepted.`);return t},queriesOver=e=>{const select=async(t,r)=>(await e(t,r)).rows;return{execute:e,select:select,selectOne:async(e,t)=>{const r=await select(e,t);return r.length>0?r[0]:null}}},createDb=(e,t)=>{let r=Promise.resolve();const exclusive=e=>{const t=r.then(e);return r=t.catch(()=>{}),t},run=async(e,r)=>{try{return await t.execute(e,r)}catch(t){throw _index.Logger.error(`[@webority/mobile-core] SQLite execute failed: ${e}`,t),t}},n=queriesOver(run),{execute:i,select:o,selectOne:a}=queriesOver((e,t)=>exclusive(()=>run(e,t))),runTransaction=async e=>{let r=!0;const n=queriesOver((e,t)=>r?run(e,t):Promise.reject(new Error("[@webority/mobile-core] A transaction handle was used after its transaction ended. Use the database itself outside transaction().")));try{let r;await t.execute("BEGIN");try{r=await e(n)}catch(e){try{await t.execute("ROLLBACK")}catch(e){_index.Logger.error("[@webority/mobile-core] SQLite ROLLBACK failed",e)}throw e}try{await t.execute("COMMIT")}catch(e){try{await t.execute("ROLLBACK")}catch{}throw e}return r}finally{r=!1}},transaction=e=>exclusive(()=>runTransaction(e)),insertSql=(e,t)=>{const r=quoteIdentifier(e,"table name"),n=Object.entries(t);if(0===n.length)throw new Error(`[@webority/mobile-core] Cannot insert an empty row into ${e}. Pass at least one column.`);const i=n.map(([e])=>quoteIdentifier(e,"column name")),o=n.map(()=>"?");return{sql:`INSERT INTO ${r} (${i.join(", ")}) VALUES (${o.join(", ")})`,params:n.map(([,e])=>toSqliteValue(e))}},findAll=async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=buildWhere(t),a=[...i.params];let s=`SELECT * FROM ${n}${i.clause}${buildOrderBy(r?.orderBy)}`;return void 0!==r?.limit?(assertNonNegativeInteger(r.limit,"limit"),s+=" LIMIT ?",a.push(r.limit)):void 0!==r?.offset&&(s+=" LIMIT -1"),void 0!==r?.offset&&(assertNonNegativeInteger(r.offset,"offset"),s+=" OFFSET ?",a.push(r.offset)),o(s,a)},ensureLedger=async()=>{await i(`CREATE TABLE IF NOT EXISTS ${escapeIdentifier(MIGRATIONS_TABLE)} (version INTEGER PRIMARY KEY, name TEXT, applied_at TEXT NOT NULL, checksum TEXT NOT NULL)`)},s=`INSERT OR REPLACE INTO ${escapeIdentifier(MIGRATIONS_TABLE)} (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)`,wipe=()=>exclusive(async()=>{const e=await n.selectOne("PRAGMA foreign_keys"),t=1===Number(e?.foreign_keys??0);await run("PRAGMA foreign_keys = OFF");try{await runTransaction(async e=>{const t=await e.select("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name <> ?",[MIGRATIONS_TABLE]);for(const r of t)await e.execute(`DELETE FROM ${escapeIdentifier(r.name)}`)})}finally{await run("PRAGMA foreign_keys = "+(t?"ON":"OFF"))}}),migrate=async(t,r)=>{const n=new Set;for(const r of t){if(n.has(r.version))throw new Error(`[@webority/mobile-core] Duplicate migration version ${r.version} for database "${e}".`);n.add(r.version)}const i=await a("PRAGMA user_version");let l=Number(i?.user_version??0);if(t.length>0&&!r?.allowDowngrade){const r=t.reduce((e,t)=>Math.max(e,t.version),0);if(l>r)throw new Error(`[@webority/mobile-core] Database "${e}" is at schema version ${l}, which is newer than the highest migration supplied (${r}). This build would run old code against a newer schema. Ship the missing migrations, or pass { allowDowngrade: true } if this build is known to be compatible.`)}await ensureLedger();const c=await o(`SELECT version, checksum FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),u=new Map(c.map(e=>[Number(e.version),String(e.checksum)]));for(const r of t){if(r.version>l)continue;const t=u.get(r.version);if(void 0===t)continue;const n=migrationChecksum(r);if(t!==n)throw new Error(`[@webority/mobile-core] Migration ${r.version} has changed since it was applied to database "${e}" (recorded ${t}, now ${n}). A released migration must never be edited; add a new one instead.`)}const f=t.filter(e=>e.version>l).sort((e,t)=>e.version-t.version);for(const e of f){const t=migrationChecksum(e);await transaction(async r=>{if("function"==typeof e.up)await e.up(r);else{const t=Array.isArray(e.up)?e.up:[e.up];for(const e of t)await r.execute(e)}await r.execute(s,[e.version,e.name??null,(new Date).toISOString(),t]),await r.execute(`PRAGMA user_version = ${e.version}`)}),l=e.version}return l};return{name:e,execute:i,select:o,selectOne:a,transaction:transaction,insert:async(e,t)=>{const{sql:r,params:n}=insertSql(e,t);return(await i(r,n)).insertId},insertMany:async(e,t)=>{if(0===t.length)return 0;const r=t.map(t=>insertSql(e,t));return await transaction(async e=>{for(const t of r)await e.execute(t.sql,t.params)}),t.length},update:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),o=Object.entries(t);if(0===o.length)throw new Error(`[@webority/mobile-core] Cannot update ${e} with no values. Pass at least one column to set.`);const a=o.map(([e])=>`${quoteIdentifier(e,"column name")} = ?`),s=o.map(([,e])=>toSqliteValue(e)),l=buildWhere(r);return(await i(`UPDATE ${n} SET ${a.join(", ")}${l.clause}`,[...s,...l.params])).rowsAffected},remove:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t);return(await i(`DELETE FROM ${r}${n.clause}`,n.params)).rowsAffected},upsert:async(e,t,r)=>{if(0===r.length)throw new Error(`[@webority/mobile-core] upsert on ${e} needs at least one conflict column; without one SQLite has no key to match an existing row on.`);const{sql:n,params:o}=insertSql(e,t),a=new Set(r),s=r.map(e=>quoteIdentifier(e,"column name")),l=Object.keys(t).filter(e=>!a.has(e)).map(e=>{const t=quoteIdentifier(e,"column name");return`${t} = excluded.${t}`}),c=l.length>0?`DO UPDATE SET ${l.join(", ")}`:"DO NOTHING";await i(`${n} ON CONFLICT (${s.join(", ")}) ${c}`,o)},findAll:findAll,findOne:async(e,t)=>{const r=await findAll(e,t,{limit:1});return r.length>0?r[0]:null},count:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t),i=await a(`SELECT COUNT(*) AS cnt FROM ${r}${n.clause}`,n.params);return Number(i?.cnt??0)},tableExists:async e=>{assertSqlIdentifier(e,"table name");return null!==await a("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",[e])},createTable:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),o=Object.entries(t);if(0===o.length)throw new Error(`[@webority/mobile-core] Cannot create table ${e} with no columns.`);const a=o.map(([e,t])=>`${quoteIdentifier(e,"column name")} ${assertColumnDefinition(t,e)}`),s=!1===r?.ifNotExists?"":"IF NOT EXISTS ";await i(`CREATE TABLE ${s}${n} (${a.join(", ")})`)},dropTable:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=!1===t?.ifExists?"":"IF EXISTS ";await i(`DROP TABLE ${n}${r}`)},wipe:wipe,reset:async e=>(await wipe(),await ensureLedger(),await transaction(async e=>{await e.execute(`DELETE FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),await e.execute("PRAGMA user_version = 0")}),migrate(e)),migrate:migrate,baseline:async(e,t)=>{assertNonNegativeInteger(e,"baseline version"),await ensureLedger();const r=(new Date).toISOString();await transaction(async n=>{for(const i of t??[])i.version>e||await n.execute(s,[i.version,i.name??null,r,migrationChecksum(i)]);await n.execute(`PRAGMA user_version = ${e}`)})},close:()=>exclusive(()=>t.close())}},openDatabase=async(e,t)=>{const r=requireBackend("open"),n=await r.open(e,t?{location:t.location}:void 0),i=createDb(e,n);if(!1!==t?.pragmas){const e={...DEFAULT_PRAGMAS,...t?.pragmas??{}};for(const[t,r]of Object.entries(e))assertSqlIdentifier(t,"PRAGMA name"),await i.execute(`PRAGMA ${t} = ${renderPragmaValue(t,r)}`)}return i};exports.openDatabase=openDatabase;
@@ -1 +1 @@
1
- "use strict";import axios from"axios";import{getConfigValue}from"../config/index.js";import{Logger}from"../logger/index.js";import"../types/axios.js";import{applyConfiguredBaseUrl}from"./baseUrl.js";import{applyClientHeaders}from"./clientHeaders.js";import{logCurl}from"./curl.js";import{readMinAppVersion}from"./minAppVersion.js";import{sendFormDataAsMultipart}from"./multipart.js";import{applyRequestDefaults}from"./requestDefaults.js";import{responseBodyText}from"./responseBodyText.js";import{attachRetry}from"./retry.js";const publicClient=axios.create({headers:{"Content-Type":"application/json",Accept:"application/json"}});applyConfiguredBaseUrl(publicClient),applyRequestDefaults(publicClient),sendFormDataAsMultipart(publicClient),readMinAppVersion(publicClient),attachRetry(publicClient),publicClient.interceptors.request.use(e=>(Logger.info(`Request: ${e.method?.toUpperCase()} ${e.url}`),getConfigValue("logCurl")&&logCurl(e),e),e=>{const r=e instanceof Error?e.message:String(e);return Logger.error("Request error:",r),Promise.reject(e)}),applyClientHeaders(publicClient),publicClient.interceptors.response.use(e=>(Logger.info(`Response: ${e.status} from ${e.config.url}`),e),e=>{if(axios.isAxiosError(e)){const r=e.config;if(r?.skipErrorLog)return Promise.reject(e);e.response?Logger.error(`Response error: ${responseBodyText(e.response.data,e.message)}, URL: ${r?.url}, Status: ${e.response.status}`):Logger.error(`Network error or no response from server: ${e.message}, URL: ${r?.url}`)}else{const r=e instanceof Error?e.message:String(e);Logger.error(`Unknown response error: ${r}`)}return Promise.reject(e)});export default publicClient;
1
+ "use strict";import axios from"axios";import{getConfigValue}from"../config/index.js";import{Logger}from"../logger/index.js";import"../types/axios.js";import{applyConfiguredBaseUrl}from"./baseUrl.js";import{applyClientHeaders}from"./clientHeaders.js";import{logCurl}from"./curl.js";import{readMinAppVersion}from"./minAppVersion.js";import{sendFormDataAsMultipart}from"./multipart.js";import{applyRequestDefaults}from"./requestDefaults.js";import{responseBodyText}from"./responseBodyText.js";import{attachRetry}from"./retry.js";const publicClient=axios.create({headers:{"Content-Type":"application/json",Accept:"application/json"}});applyConfiguredBaseUrl(publicClient),applyRequestDefaults(publicClient),sendFormDataAsMultipart(publicClient),readMinAppVersion(publicClient),attachRetry(publicClient),publicClient.interceptors.request.use(e=>(Logger.info(`Request: ${e.method?.toUpperCase()} ${e.url}`),getConfigValue("logCurl")&&logCurl(e),e),e=>{const r=e instanceof Error?e.message:String(e);return Logger.error("Request error:",r),Promise.reject(e)}),applyClientHeaders(publicClient),publicClient.interceptors.response.use(e=>(Logger.info(`Response: ${e.status} from ${e.config.url}`),e),e=>{if(axios.isCancel(e))return Promise.reject(e);if(axios.isAxiosError(e)){const r=e.config;if(r?.skipErrorLog)return Promise.reject(e);e.response?Logger.error(`Response error: ${responseBodyText(e.response.data,e.message)}, URL: ${r?.url}, Status: ${e.response.status}`):Logger.error(`Network error or no response from server: ${e.message}, URL: ${r?.url}`)}else{const r=e instanceof Error?e.message:String(e);Logger.error(`Unknown response error: ${r}`)}return Promise.reject(e)});export default publicClient;
@@ -1 +1 @@
1
- "use strict";import axios,{AxiosHeaders}from"axios";import{ACCESS_TOKEN_KEY,getToken}from"../auth/authStore.js";import{getConfigValue}from"../config/index.js";import{Logger}from"../logger/index.js";import{getLastKnownNetworkStatus}from"../network/networkStatus.js";import"../types/axios.js";import{applyConfiguredBaseUrl}from"./baseUrl.js";import{applyAccountIdHeader,applyClientHeaders}from"./clientHeaders.js";import{logCurl}from"./curl.js";import{readMinAppVersion}from"./minAppVersion.js";import{sendFormDataAsMultipart}from"./multipart.js";import{applyRequestDefaults}from"./requestDefaults.js";import{responseBodyText}from"./responseBodyText.js";import{attachRetry}from"./retry.js";import{endSessionAndLogout,getAccountIdProvider,getErrorMessage,getRefreshPath,isRefreshInFlight,queueBehindRefresh,readSignOutReason,refreshAccessToken,resetRefreshState}from"./tokenRefresh.js";export{injectStore,injectUserAuth,resetRefreshState,SIGN_OUT_REASON_HEADER}from"./tokenRefresh.js";export class RequestOfflineError extends Error{constructor(e="Request blocked: device is offline"){super(e),this.name="RequestOfflineError",Object.setPrototypeOf(this,RequestOfflineError.prototype)}}export const SESSION_TERMINATION_CODES={TERMINATED:"SESSION_TERMINATED",EXPIRED:"SESSION_EXPIRED"};const TERMINATION_CODES=new Set(Object.values(SESSION_TERMINATION_CODES));export const SESSION_INVALID_HEADER="x-security-stamp-invalid";const hasSessionInvalidHeader=e=>{if(!e||"object"!=typeof e)return!1;const r=e.get,t="function"==typeof r?r.call(e,SESSION_INVALID_HEADER):e[SESSION_INVALID_HEADER];return"true"===String(t).toLowerCase()},getSessionTerminationCode=e=>{if(!e||"object"!=typeof e)return;const r=e,t=r.error,s=[r.code,r.errorCode,t&&"object"==typeof t?t.code:void 0];for(const e of s)if("string"==typeof e&&TERMINATION_CODES.has(e))return e},userClient=axios.create({headers:{"Content-Type":"application/json"}});readMinAppVersion(userClient),attachRetry(userClient);const setAuthHeader=(e,r)=>{e.headers instanceof AxiosHeaders?e.headers.set("Authorization",`Bearer ${r}`):e.headers=AxiosHeaders.from({...e.headers,Authorization:`Bearer ${r}`})},isRefreshTokenRequest=e=>{const r=getRefreshPath();return!!r&&!!e?.includes(r)};applyConfiguredBaseUrl(userClient),applyRequestDefaults(userClient),sendFormDataAsMultipart(userClient),userClient.interceptors.request.use(e=>(getConfigValue("logCurl")&&logCurl(e),e)),applyClientHeaders(userClient),userClient.interceptors.request.use(async e=>{if(!1===(await getLastKnownNetworkStatus()).isConnected)return Logger.warn(`Request blocked: device is offline. URL: ${e.url}`),Promise.reject(new RequestOfflineError);try{if(isRefreshTokenRequest(e.url))return e;const r=await getToken(ACCESS_TOKEN_KEY);if(!r?.token)throw new Error("No access token available");const t=Date.now(),s=Date.parse(r.expiry);if(s<=t+3e4){Logger.info("Access token expired or about to expire, refreshing before request");const r=await refreshAccessToken();return setAuthHeader(e,r),await applyAccountIdHeader(e.headers,getAccountIdProvider()),e}return setAuthHeader(e,r.token),await applyAccountIdHeader(e.headers,getAccountIdProvider()),e}catch(e){return Logger.error("Failed to attach token:",getErrorMessage(e)),Promise.reject(e)}},e=>(Logger.error("Request error:",getErrorMessage(e)),Promise.reject(e))),userClient.interceptors.response.use(e=>e,async e=>{if(!axios.isAxiosError(e))return Logger.error(`Non-axios error in response interceptor: ${getErrorMessage(e)}`),Promise.reject(e);const r=e,t=r.config;if(!r.response)return t?.skipErrorLog||Logger.error(`Network error or no response from server: ${r.message}, URL: ${t?.url}`),Promise.reject(e);if(401===r.response.status&&t&&!t._retry){if(isRefreshTokenRequest(t.url))return Promise.reject(e);const s=readSignOutReason(r.response.headers),o=getSessionTerminationCode(r.response.data)??(hasSessionInvalidHeader(r.response.headers)||null!==s?SESSION_TERMINATION_CODES.TERMINATED:void 0);if(o)return Logger.warn(`Session ${o}, logging out without refresh. URL: ${t.url}`),resetRefreshState(),await endSessionAndLogout("terminated",s),Promise.reject(e);if(Logger.warn(`Unauthorized URL: ${t.url}`),t._retry=!0,isRefreshInFlight())return queueBehindRefresh().then(e=>(Logger.info("Retrying request after token refresh:",t.url),setAuthHeader(t,e),userClient(t))).catch(e=>(Logger.error("Failed to retry request after token refresh:",getErrorMessage(e)),Promise.reject(e)));try{const e=await refreshAccessToken();return setAuthHeader(t,e),userClient(t)}catch(e){return Promise.reject(e)}}return t?.skipErrorLog||Logger.error(`Response error: ${responseBodyText(r.response?.data,r.message)}, URL: ${t?.url}, Status: ${r.response.status}`),Promise.reject(e)});export default userClient;
1
+ "use strict";import axios,{AxiosHeaders}from"axios";import{ACCESS_TOKEN_KEY,getToken}from"../auth/authStore.js";import{getConfigValue}from"../config/index.js";import{Logger}from"../logger/index.js";import{getLastKnownNetworkStatus}from"../network/networkStatus.js";import"../types/axios.js";import{applyConfiguredBaseUrl}from"./baseUrl.js";import{applyAccountIdHeader,applyClientHeaders}from"./clientHeaders.js";import{logCurl}from"./curl.js";import{readMinAppVersion}from"./minAppVersion.js";import{sendFormDataAsMultipart}from"./multipart.js";import{applyRequestDefaults}from"./requestDefaults.js";import{responseBodyText}from"./responseBodyText.js";import{attachRetry}from"./retry.js";import{endSessionAndLogout,getAccountIdProvider,getErrorMessage,getRefreshPath,isRefreshInFlight,queueBehindRefresh,readSignOutReason,refreshAccessToken,resetRefreshState}from"./tokenRefresh.js";export{injectStore,injectUserAuth,resetRefreshState,SIGN_OUT_REASON_HEADER}from"./tokenRefresh.js";export class RequestOfflineError extends Error{constructor(e="Request blocked: device is offline"){super(e),this.name="RequestOfflineError",Object.setPrototypeOf(this,RequestOfflineError.prototype)}}class NoAccessTokenError extends Error{constructor(){super("No access token available"),this.name="NoAccessTokenError",Object.setPrototypeOf(this,NoAccessTokenError.prototype)}}export const SESSION_TERMINATION_CODES={TERMINATED:"SESSION_TERMINATED",EXPIRED:"SESSION_EXPIRED"};const TERMINATION_CODES=new Set(Object.values(SESSION_TERMINATION_CODES));export const SESSION_INVALID_HEADER="x-security-stamp-invalid";const hasSessionInvalidHeader=e=>{if(!e||"object"!=typeof e)return!1;const r=e.get,t="function"==typeof r?r.call(e,SESSION_INVALID_HEADER):e[SESSION_INVALID_HEADER];return"true"===String(t).toLowerCase()},getSessionTerminationCode=e=>{if(!e||"object"!=typeof e)return;const r=e,t=r.error,s=[r.code,r.errorCode,t&&"object"==typeof t?t.code:void 0];for(const e of s)if("string"==typeof e&&TERMINATION_CODES.has(e))return e},userClient=axios.create({headers:{"Content-Type":"application/json"}});readMinAppVersion(userClient),attachRetry(userClient);const setAuthHeader=(e,r)=>{e.headers instanceof AxiosHeaders?e.headers.set("Authorization",`Bearer ${r}`):e.headers=AxiosHeaders.from({...e.headers,Authorization:`Bearer ${r}`})},isRefreshTokenRequest=e=>{const r=getRefreshPath();return!!r&&!!e?.includes(r)};applyConfiguredBaseUrl(userClient),applyRequestDefaults(userClient),sendFormDataAsMultipart(userClient),userClient.interceptors.request.use(e=>(getConfigValue("logCurl")&&logCurl(e),e)),applyClientHeaders(userClient),userClient.interceptors.request.use(async e=>{if(!1===(await getLastKnownNetworkStatus()).isConnected)return Logger.warn(`Request blocked: device is offline. URL: ${e.url}`),Promise.reject(new RequestOfflineError);try{if(isRefreshTokenRequest(e.url))return e;const r=await getToken(ACCESS_TOKEN_KEY);if(!r?.token)throw new NoAccessTokenError;const t=Date.now(),s=Date.parse(r.expiry);if(s<=t+3e4){Logger.info("Access token expired or about to expire, refreshing before request");const r=await refreshAccessToken();return setAuthHeader(e,r),await applyAccountIdHeader(e.headers,getAccountIdProvider()),e}return setAuthHeader(e,r.token),await applyAccountIdHeader(e.headers,getAccountIdProvider()),e}catch(r){return r instanceof NoAccessTokenError?Logger.info(`Request not sent: signed out. URL: ${e.url}`):Logger.error("Failed to attach token:",getErrorMessage(r)),Promise.reject(r)}},e=>(Logger.error("Request error:",getErrorMessage(e)),Promise.reject(e))),userClient.interceptors.response.use(e=>e,async e=>{if(axios.isCancel(e))return Promise.reject(e);if(e instanceof RequestOfflineError||e instanceof NoAccessTokenError)return Promise.reject(e);if(!axios.isAxiosError(e))return Logger.error(`Non-axios error in response interceptor: ${getErrorMessage(e)}`),Promise.reject(e);const r=e,t=r.config;if(!r.response)return t?.skipErrorLog||Logger.error(`Network error or no response from server: ${r.message}, URL: ${t?.url}`),Promise.reject(e);if(401===r.response.status&&t&&!t._retry){if(isRefreshTokenRequest(t.url))return Promise.reject(e);const s=readSignOutReason(r.response.headers),o=getSessionTerminationCode(r.response.data)??(hasSessionInvalidHeader(r.response.headers)||null!==s?SESSION_TERMINATION_CODES.TERMINATED:void 0);if(o)return Logger.warn(`Session ${o}, logging out without refresh. URL: ${t.url}`),resetRefreshState(),await endSessionAndLogout("terminated",s),Promise.reject(e);if(Logger.warn(`Unauthorized URL: ${t.url}`),t._retry=!0,isRefreshInFlight())return queueBehindRefresh().then(e=>(Logger.info("Retrying request after token refresh:",t.url),setAuthHeader(t,e),userClient(t))).catch(e=>(Logger.error("Failed to retry request after token refresh:",getErrorMessage(e)),Promise.reject(e)));try{const e=await refreshAccessToken();return setAuthHeader(t,e),userClient(t)}catch(e){return Promise.reject(e)}}return t?.skipErrorLog||Logger.error(`Response error: ${responseBodyText(r.response?.data,r.message)}, URL: ${t?.url}, Status: ${r.response.status}`),Promise.reject(e)});export default userClient;
@@ -1 +1 @@
1
- "use strict";import{getConfigValue}from"../config/index.js";import{redact,redactString}from"./redact.js";const Colors={RESET:"",GRAY:"",BLUE:"",YELLOW:"",RED_BOLD:""},LogLevels={DEBUG:{level:"debug",color:Colors.GRAY},INFO:{level:"info",color:Colors.BLUE},WARN:{level:"warn",color:Colors.YELLOW},ERROR:{level:"error",color:Colors.RED_BOLD}},isLogLevelAllowed=e=>{const r=["debug","info","warn","error"],o=getConfigValue("logLevel","info");return r.indexOf(e)>=r.indexOf(o)},isEnabled=()=>getConfigValue("loggingEnabled",!0);let remote=null;export const setRemoteLogger=e=>{remote=e};const safe=e=>{if(e)try{e()}catch{}},toErrorObject=e=>{if(e instanceof Error)return e;if("string"==typeof e)return new Error(e);try{return new Error(JSON.stringify(e))}catch{return new Error(String(e))}};let deviceContext=null;const normalizeAttributeValue=e=>null==e?null:"string"==typeof e||"number"==typeof e||"boolean"==typeof e?e:String(e);export const setDeviceContext=e=>{if(deviceContext=e,e)for(const[r,o]of Object.entries(e))safe(()=>remote?.setAttribute?.(r,normalizeAttributeValue(o)))};export const getDeviceContext=()=>deviceContext;const DEFAULT_LOGGER_OPTIONS={dedupeWindowMs:5e3,dedupeMaxPerWindow:1,screenHistoryLimit:20};let loggerOptions={...DEFAULT_LOGGER_OPTIONS};export const configureLogger=e=>{loggerOptions={...loggerOptions,...e},screenHistory.length>loggerOptions.screenHistoryLimit&&(screenHistory=screenHistory.slice(screenHistory.length-loggerOptions.screenHistoryLimit))};let currentScreen=null,screenHistory=[];const dedupeMap=new Map,MAX_DEDUPE_ENTRIES=200,dedupeKeyFor=e=>{const r=e.stack?.split("\n")[1]?.trim();return r?`${e.message}::${r}`:e.message},capDedupeMap=()=>{for(;dedupeMap.size>200;){const e=dedupeMap.keys().next().value;if(void 0===e)return;dedupeMap.delete(e)}},shouldForward=e=>{if(loggerOptions.dedupeWindowMs<=0)return{forward:!0};const r=Date.now(),o=dedupeMap.get(e);if(!o||r-o.windowStart>=loggerOptions.dedupeWindowMs){const t=o?.suppressedCount??0;return dedupeMap.set(e,{windowStart:r,count:1,suppressedCount:0}),capDedupeMap(),t>0?{forward:!0,suppressedCount:t}:{forward:!0}}return o.count+=1,o.count>loggerOptions.dedupeMaxPerWindow?(o.suppressedCount+=1,{forward:!1}):{forward:!0}},buildErrorContext=(e,r)=>{const o={...deviceContext};return null!==currentScreen&&(o.screen=currentScreen),r&&(o.suppressedCount=r),{...o,...e}},forwardErrorToRemote=(e,r)=>{const o=dedupeKeyFor(e),{forward:t,suppressedCount:n}=shouldForward(o);t&&safe(()=>remote?.recordError?.(e,buildErrorContext(r,n)))},logDebug=(e,...r)=>{isEnabled()&&isLogLevelAllowed(LogLevels.DEBUG.level)&&console.debug(`${LogLevels.DEBUG.color}[DEBUG]: ${redactString(e)}${Colors.RESET}`,...redact(r))},logInfo=(e,...r)=>{const o=redactString(e),t=redact(r);isEnabled()&&isLogLevelAllowed(LogLevels.INFO.level)&&(console.info(`${LogLevels.INFO.color}[INFO]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("info",o,t)))},logWarning=(e,...r)=>{const o=redactString(e),t=redact(r);isEnabled()&&isLogLevelAllowed(LogLevels.WARN.level)&&(console.warn(`${LogLevels.WARN.color}[WARNING]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("warn",o,t)))},logError=(e,...r)=>{const o=redactString(e),t=redact(r);isEnabled()&&isLogLevelAllowed(LogLevels.ERROR.level)&&console.error(`${LogLevels.ERROR.color}[ERROR]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("error",o,t));const n=t[0];void 0!==n&&forwardErrorToRemote(toErrorObject(n),{source:"Logger.error",message:o})},breadcrumb=(e,r)=>{const o=redactString(e),t=redact(r);isEnabled()&&isLogLevelAllowed("info")&&console.info(`${Colors.BLUE}[BREADCRUMB]: ${o}${Colors.RESET}`,t??""),safe(()=>remote?.breadcrumb?.(o,t))},recordError=(e,r)=>{const o=redact(toErrorObject(e)),t=redact(r);isEnabled()&&isLogLevelAllowed("error")&&console.error(`${Colors.RED_BOLD}[RECORDED]: ${o.message}${Colors.RESET}`,t??""),forwardErrorToRemote(o,t)},asDict=e=>"object"==typeof e&&null!==e?e:null,headerValue=(e,r)=>{const o=asDict(e);if(!o)return null;const t="function"==typeof o.get?o.get.call(e,r):o[r]??o[r.toLowerCase()];return"string"==typeof t&&""!==t?t:null},requestFailed=(e,r={})=>{const{label:o,expectedStatuses:t=[],...n}=r,s=asDict(e),l=asDict(s?.config),c=asDict(s?.response),i=asDict(c?.data),d="number"==typeof c?.status?c.status:null,a="string"==typeof l?.method?l.method.toUpperCase():null,g="string"==typeof l?.url?l.url.split("?")[0]??null:null,u=o??([a,g].filter(Boolean).join(" ")||"Request");if("CanceledError"===s?.name||"ERR_CANCELED"===s?.code||!0===s?.__CANCEL__)return void logInfo(`${u} canceled`);if(null!==d&&t.includes(d))return void logWarning(`${u} refused (${d})`);const E="string"==typeof s?.message?s.message:e instanceof Error?e.message:null,f=redactString(null===d?`${u} failed`:`${u} failed (${d})`);isEnabled()&&isLogLevelAllowed(LogLevels.ERROR.level)&&console.error(`${LogLevels.ERROR.color}[ERROR]: ${f}${Colors.RESET}`,redactString(E??"")),safe(()=>remote?.log?.("error",f,[]));const p=getConfigValue("correlationIdHeader",null),L=redact({source:"Logger.requestFailed",label:u,status:d,method:a,path:g,code:"string"==typeof i?.code?i.code:null,traceId:"string"==typeof i?.traceId?i.traceId:null,correlationId:p?headerValue(l?.headers,p):null,detail:E,...n});forwardErrorToRemote(redact(new Error(f)),L)},setUser=(e,r)=>{safe(()=>remote?.setUser?.(e,r))},setAttribute=(e,r)=>{safe(()=>remote?.setAttribute?.(e,r))},trackScreen=(e,r)=>{e!==currentScreen&&(currentScreen=e,screenHistory.push(e),screenHistory.length>loggerOptions.screenHistoryLimit&&(screenHistory=screenHistory.slice(screenHistory.length-loggerOptions.screenHistoryLimit)),breadcrumb(`Screen: ${e}`,r))},getCurrentScreen=()=>currentScreen,getScreenHistory=()=>[...screenHistory];export const Logger={debug:logDebug,info:logInfo,warn:logWarning,error:logError,breadcrumb:breadcrumb,recordError:recordError,requestFailed:requestFailed,setUser:setUser,setAttribute:setAttribute,trackScreen:trackScreen,getCurrentScreen:getCurrentScreen,getScreenHistory:getScreenHistory};
1
+ "use strict";import{getConfigValue}from"../config/index.js";import{redact,redactString}from"./redact.js";const Colors={RESET:"",GRAY:"",BLUE:"",YELLOW:"",RED_BOLD:""},LogLevels={DEBUG:{level:"debug",color:Colors.GRAY},INFO:{level:"info",color:Colors.BLUE},WARN:{level:"warn",color:Colors.YELLOW},ERROR:{level:"error",color:Colors.RED_BOLD}},isLogLevelAllowed=e=>{const r=["debug","info","warn","error"],o=getConfigValue("logLevel","info");return r.indexOf(e)>=r.indexOf(o)},isEnabled=()=>getConfigValue("loggingEnabled",!0);let remote=null;export const setRemoteLogger=e=>{remote=e};const safe=e=>{if(e)try{e()}catch{}},toErrorObject=e=>{if(e instanceof Error)return e;if("string"==typeof e)return new Error(e);try{return new Error(JSON.stringify(e))}catch{return new Error(String(e))}};let deviceContext=null;const normalizeAttributeValue=e=>null==e?null:"string"==typeof e||"number"==typeof e||"boolean"==typeof e?e:String(e);export const setDeviceContext=e=>{if(deviceContext=e,e)for(const[r,o]of Object.entries(e))safe(()=>remote?.setAttribute?.(r,normalizeAttributeValue(o)))};export const getDeviceContext=()=>deviceContext;const DEFAULT_LOGGER_OPTIONS={dedupeWindowMs:5e3,dedupeMaxPerWindow:1,screenHistoryLimit:20};let loggerOptions={...DEFAULT_LOGGER_OPTIONS};export const configureLogger=e=>{loggerOptions={...loggerOptions,...e},screenHistory.length>loggerOptions.screenHistoryLimit&&(screenHistory=screenHistory.slice(screenHistory.length-loggerOptions.screenHistoryLimit))};let currentScreen=null,screenHistory=[];const dedupeMap=new Map,MAX_DEDUPE_ENTRIES=200,dedupeKeyFor=e=>{const r=e.stack?.split("\n")[1]?.trim();return r?`${e.message}::${r}`:e.message},capDedupeMap=()=>{for(;dedupeMap.size>200;){const e=dedupeMap.keys().next().value;if(void 0===e)return;dedupeMap.delete(e)}},shouldForward=e=>{if(loggerOptions.dedupeWindowMs<=0)return{forward:!0};const r=Date.now(),o=dedupeMap.get(e);if(!o||r-o.windowStart>=loggerOptions.dedupeWindowMs){const t=o?.suppressedCount??0;return dedupeMap.set(e,{windowStart:r,count:1,suppressedCount:0}),capDedupeMap(),t>0?{forward:!0,suppressedCount:t}:{forward:!0}}return o.count+=1,o.count>loggerOptions.dedupeMaxPerWindow?(o.suppressedCount+=1,{forward:!1}):{forward:!0}},buildErrorContext=(e,r)=>{const o={...deviceContext};return null!==currentScreen&&(o.screen=currentScreen),r&&(o.suppressedCount=r),{...o,...e}},forwardErrorToRemote=(e,r)=>{const o=dedupeKeyFor(e),{forward:t,suppressedCount:n}=shouldForward(o);t&&safe(()=>remote?.recordError?.(e,buildErrorContext(r,n)))},logDebug=(e,...r)=>{isEnabled()&&isLogLevelAllowed(LogLevels.DEBUG.level)&&console.debug(`${LogLevels.DEBUG.color}[DEBUG]: ${redactString(e)}${Colors.RESET}`,...redact(r))},logInfo=(e,...r)=>{const o=redactString(e),t=redact(r);isEnabled()&&isLogLevelAllowed(LogLevels.INFO.level)&&(console.info(`${LogLevels.INFO.color}[INFO]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("info",o,t)))},logWarning=(e,...r)=>{const o=redactString(e),t=redact(r);isEnabled()&&isLogLevelAllowed(LogLevels.WARN.level)&&(console.warn(`${LogLevels.WARN.color}[WARNING]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("warn",o,t)))},logError=(e,...r)=>{const o=redactString(e),t=redact(r);isEnabled()&&isLogLevelAllowed(LogLevels.ERROR.level)&&console.error(`${LogLevels.ERROR.color}[ERROR]: ${o}${Colors.RESET}`,...t),safe(()=>remote?.log?.("error",o,t));const n=t[0];void 0!==n&&forwardErrorToRemote(toErrorObject(n),{source:"Logger.error",message:o})},breadcrumb=(e,r)=>{const o=redactString(e),t=redact(r);isEnabled()&&isLogLevelAllowed("info")&&console.info(`${Colors.BLUE}[BREADCRUMB]: ${o}${Colors.RESET}`,t??""),safe(()=>remote?.breadcrumb?.(o,t))},recordError=(e,r)=>{const o=redact(toErrorObject(e)),t=redact(r);isEnabled()&&isLogLevelAllowed("error")&&console.error(`${Colors.RED_BOLD}[RECORDED]: ${o.message}${Colors.RESET}`,t??""),forwardErrorToRemote(o,t)},asDict=e=>"object"==typeof e&&null!==e?e:null,headerValue=(e,r)=>{const o=asDict(e);if(!o)return null;const t="function"==typeof o.get?o.get.call(e,r):o[r]??o[r.toLowerCase()];return"string"==typeof t&&""!==t?t:null},requestFailed=(e,r={})=>{const{label:o,expectedStatuses:t=[],...n}=r,s=asDict(e),l=asDict(s?.config),c=asDict(s?.response),i=asDict(c?.data),d="number"==typeof c?.status?c.status:null,a="string"==typeof l?.method?l.method.toUpperCase():null,u="string"==typeof l?.url?l.url.split("?")[0]??null:null,g=o??([a,u].filter(Boolean).join(" ")||"Request");if("CanceledError"===s?.name||"ERR_CANCELED"===s?.code||!0===s?.__CANCEL__)return void logInfo(`${g} canceled`);if(null!==d&&t.includes(d))return void logInfo(`${g} refused (${d})`);const E="string"==typeof s?.message?s.message:e instanceof Error?e.message:null,f=redactString(null===d?`${g} failed`:`${g} failed (${d})`);isEnabled()&&isLogLevelAllowed(LogLevels.ERROR.level)&&console.error(`${LogLevels.ERROR.color}[ERROR]: ${f}${Colors.RESET}`,redactString(E??"")),safe(()=>remote?.log?.("error",f,[]));const p=getConfigValue("correlationIdHeader",null),L=redact({source:"Logger.requestFailed",label:g,status:d,method:a,path:u,code:"string"==typeof i?.code?i.code:null,traceId:"string"==typeof i?.traceId?i.traceId:null,correlationId:p?headerValue(l?.headers,p):null,detail:E,...n});forwardErrorToRemote(redact(new Error(f)),L)},setUser=(e,r)=>{safe(()=>remote?.setUser?.(e,r))},setAttribute=(e,r)=>{safe(()=>remote?.setAttribute?.(e,r))},trackScreen=(e,r)=>{e!==currentScreen&&(currentScreen=e,screenHistory.push(e),screenHistory.length>loggerOptions.screenHistoryLimit&&(screenHistory=screenHistory.slice(screenHistory.length-loggerOptions.screenHistoryLimit)),breadcrumb(`Screen: ${e}`,r))},getCurrentScreen=()=>currentScreen,getScreenHistory=()=>[...screenHistory];export const Logger={debug:logDebug,info:logInfo,warn:logWarning,error:logError,breadcrumb:breadcrumb,recordError:recordError,requestFailed:requestFailed,setUser:setUser,setAttribute:setAttribute,trackScreen:trackScreen,getCurrentScreen:getCurrentScreen,getScreenHistory:getScreenHistory};
@@ -1 +1 @@
1
- "use strict";export{disablePush,enablePush,initPush,notifyLocal,onAction,setBadgeCount,setCategories,setPushImplementation,usePush}from"./push.js";export{PUSH_STATUS}from"./status.js";
1
+ "use strict";export{disablePush,enablePush,initPush,notifyLocal,onAction,onReceive,resetPush,setBadgeCount,setCategories,setPushImplementation,usePush}from"./push.js";export{PUSH_STATUS}from"./status.js";
@@ -1 +1 @@
1
- "use strict";import{useSyncExternalStore}from"react";import{Platform}from"react-native";import{Logger}from"../logger/index.js";let resolved,override=null;export const setPushImplementation=e=>{override=e};export const resolveModules=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{if(require("expo").isRunningInExpoGo())return resolved=null,resolved;const e=require("expo-notifications"),t=require("expo-application");resolved={notifications:e,applicationId:t.applicationId}}catch(e){Logger.warn("[push] expo-notifications is not available; push is off",e),resolved=null}return resolved};let state={status:"unknown",enabled:!1};const subscribers=new Set,setState=e=>{state={...state,...e};for(const e of subscribers)e()};let config=null,channelsReady=Promise.resolve(),tokenSubscription=null,actionHandler=null,listening=!1;const pendingResponses=[],routedAt=new Map,REPEAT_WINDOW_MS=1e4,requireConfig=()=>{if(!config)throw new Error("[@webority/mobile-core] call initPush before enablePush or disablePush.");return config},toPermissionStatus=e=>e.granted?"granted":"denied"===e.status?"denied":"unknown",toStringData=e=>{const t={};for(const[n,o]of Object.entries(e??{}))null!=o&&(t[n]="object"==typeof o?JSON.stringify(o):String(o));return t},contentData=e=>{const t=e.notification.request.content;if(t.data)return toStringData(t.data);if("string"==typeof t.dataString)try{return toStringData(JSON.parse(t.dataString))}catch(e){Logger.error("[push] notification data is not JSON",e)}return{}},triggerData=e=>{const t=e.notification.request.trigger,n={...t?.remoteMessage?.data??{}};for(const[e,o]of Object.entries(t?.payload??{}))"aps"===e||"body"===e&&"object"==typeof o||(n[e]=o);return toStringData(n)},responseData=e=>({...triggerData(e),...contentData(e)});export const toPushAction=e=>({actionId:e.actionIdentifier,...e.userText?{userText:e.userText}:{},data:responseData(e),notificationId:e.notification.request.identifier});export const toNotificationActions=e=>e.map(e=>({identifier:e.id,buttonTitle:e.title,...e.textInput?{textInput:{submitButtonTitle:e.textInput.submitTitle,placeholder:e.textInput.placeholder}}:{},options:{opensAppToForeground:e.opensApp??!0,isDestructive:e.destructive??!1,isAuthenticationRequired:e.authenticationRequired??!1}}));const routeResponse=async(e,t)=>{const n=`${t.notification.request.identifier}:${t.actionIdentifier}`,o=routedAt.get(n);if(void 0!==o&&Date.now()-o<1e4)return;if(t.actionIdentifier!==e.DEFAULT_ACTION_IDENTIFIER&&actionHandler){routedAt.set(n,Date.now());try{await actionHandler(toPushAction(t))}catch(e){Logger.error("[push] onAction threw",e)}}else if(config){routedAt.set(n,Date.now());try{config.onOpen(responseData(t))}catch(e){Logger.error("[push] onOpen threw",e)}}else pendingResponses.push(t)},drainPending=async e=>{const t=pendingResponses.splice(0);for(const n of t)await routeResponse(e,n)},listenForResponses=e=>{listening||(listening=!0,e.addNotificationResponseReceivedListener(t=>{routeResponse(e,t)}),e.getLastNotificationResponseAsync().then(async t=>(t&&await routeResponse(e,t),e.clearLastNotificationResponseAsync?.())).catch(e=>Logger.error("[push] reading the launch notification failed",e)))};export const routeTaskPayload=async e=>{const t=resolveModules(),n=e;t&&"string"==typeof n?.actionIdentifier&&"string"==typeof n.notification?.request?.identifier&&await routeResponse(t.notifications,n)};const createChannels=async(e,t)=>{for(const n of t)try{await e.setNotificationChannelAsync(n.id,{name:n.name,importance:"high"===n.importance?e.AndroidImportance.HIGH:e.AndroidImportance.DEFAULT})}catch(e){Logger.error(`[push] creating channel ${n.id} failed`,e)}},toDevice=async(e,t)=>{const n="android"===e.type?"Fcm":"ios"===e.type?"Apns":null;if(!n||"string"!=typeof e.data)return null;if(!t)throw new Error("[@webority/mobile-core] push needs the app bundle id from expo-application.");return{installationId:await requireConfig().getInstallationId(),platform:n,token:e.data,appId:t}};export const initPush=e=>{const t=null===config;if(config=e,!t)return;const n=resolveModules();if(!n)return void setState({status:"unavailable"});const o=n.notifications;o.setNotificationHandler({handleNotification:async()=>{const e=config?.showInForeground?.()??!0;return{shouldShowBanner:e,shouldShowList:e,shouldPlaySound:e&&(config?.playSoundInForeground?.()??!0),shouldSetBadge:!0}}}),"android"===Platform.OS&&e.androidChannels?.length&&(channelsReady=createChannels(o,e.androidChannels)),listenForResponses(o),drainPending(o),o.getPermissionsAsync().then(e=>setState({status:toPermissionStatus(e)})).catch(e=>Logger.error("[push] permission check failed",e))};let registering=null,registeredKey=null;const registerOnce=e=>{const t=JSON.stringify(e);if(registering?.key===t)return registering.promise;const n=requireConfig().register(e).then(()=>{registeredKey=t}).finally(()=>{registering?.promise===n&&(registering=null)});return registering={key:t,promise:n},n};export const enablePush=async()=>{requireConfig();const e=resolveModules();if(!e)return setState({status:"unavailable"}),"unavailable";const t=e.notifications;await channelsReady;let n=await t.getPermissionsAsync();if(n.granted||(n=await t.requestPermissionsAsync()),!n.granted)return setState({status:"denied"}),"denied";const o=await toDevice(await t.getDevicePushTokenAsync(),e.applicationId);return o?(await registerOnce(o),tokenSubscription?.remove(),tokenSubscription=t.addPushTokenListener(t=>{toDevice(t,e.applicationId).then(e=>e&&JSON.stringify(e)!==registeredKey?registerOnce(e):void 0).catch(e=>Logger.error("[push] re-registering a new token failed",e))}),setState({status:"granted",enabled:!0}),"granted"):(setState({status:"unavailable"}),"unavailable")};export const disablePush=async()=>{const e=requireConfig();tokenSubscription?.remove(),tokenSubscription=null,registeredKey=null,setState({enabled:!1}),await e.unregister(await e.getInstallationId())};export const setBadgeCount=async e=>{const t=resolveModules();t&&await t.notifications.setBadgeCountAsync(e)};export const setCategories=async e=>{const t=resolveModules();if(t)for(const n of e)await t.notifications.setNotificationCategoryAsync(n.id,toNotificationActions(n.actions))};export const onAction=e=>{actionHandler=e;const t=resolveModules();return t&&(listenForResponses(t.notifications),drainPending(t.notifications)),()=>{actionHandler===e&&(actionHandler=null)}};export const notifyLocal=async({title:e,body:t,data:n})=>{const o=resolveModules();o&&await o.notifications.scheduleNotificationAsync({content:{title:e,body:t,data:n??{}},trigger:null})};const subscribe=e=>(subscribers.add(e),()=>{subscribers.delete(e)}),getSnapshot=()=>state,enable=async()=>{await enablePush()};export const usePush=()=>({...useSyncExternalStore(subscribe,getSnapshot,getSnapshot),enable:enable,disable:disablePush});
1
+ "use strict";import{useSyncExternalStore}from"react";import{Platform}from"react-native";import{Logger}from"../logger/index.js";let resolved,override=null;export const setPushImplementation=e=>{override=e};export const resolveModules=()=>{if(override)return override;if(void 0!==resolved)return resolved;try{if(require("expo").isRunningInExpoGo())return resolved=null,resolved;const e=require("expo-notifications"),t=require("expo-application");resolved={notifications:e,applicationId:t.applicationId,getApnsEnvironment:()=>t.getIosPushNotificationServiceEnvironmentAsync()}}catch(e){Logger.warn("[push] expo-notifications is not available; push is off",e),resolved=null}return resolved};let state={status:"unknown",enabled:!1};const subscribers=new Set,setState=e=>{state={...state,...e};for(const e of subscribers)e()};let config=null,channelsReady=Promise.resolve(),tokenSubscription=null,actionHandler=null;const receiveHandlers=new Set;let listening=!1,listeningForReceived=!1;const pendingResponses=[],routedAt=new Map,REPEAT_WINDOW_MS=1e4,requireConfig=()=>{if(!config)throw new Error("[@webority/mobile-core] call initPush before enablePush or disablePush.");return config},toPermissionStatus=e=>e.granted?"granted":"denied"===e.status?"denied":"unknown",toStringData=e=>{const t={};for(const[n,o]of Object.entries(e??{}))null!=o&&(t[n]="object"==typeof o?JSON.stringify(o):String(o));return t},contentData=e=>{const t=e.request.content;if(t.data)return toStringData(t.data);if("string"==typeof t.dataString)try{return toStringData(JSON.parse(t.dataString))}catch(e){Logger.error("[push] notification data is not JSON",e)}return{}},triggerData=e=>{const t=e.request.trigger,n={...t?.remoteMessage?.data??{}};for(const[e,o]of Object.entries(t?.payload??{}))"aps"===e||"body"===e&&"object"==typeof o||(n[e]=o);return toStringData(n)},notificationData=e=>({...triggerData(e),...contentData(e)}),responseData=e=>notificationData(e.notification);export const toPushReceived=e=>({notificationId:e.request.identifier,title:e.request.content.title??null,body:e.request.content.body??null,data:notificationData(e)});export const toPushAction=e=>({actionId:e.actionIdentifier,...e.userText?{userText:e.userText}:{},data:responseData(e),notificationId:e.notification.request.identifier});export const toNotificationActions=e=>e.map(e=>({identifier:e.id,buttonTitle:e.title,...e.textInput?{textInput:{submitButtonTitle:e.textInput.submitTitle,placeholder:e.textInput.placeholder}}:{},options:{opensAppToForeground:e.opensApp??!0,isDestructive:e.destructive??!1,isAuthenticationRequired:e.authenticationRequired??!1}}));const routeResponse=async(e,t)=>{const n=`${t.notification.request.identifier}:${t.actionIdentifier}`,o=routedAt.get(n);if(void 0!==o&&Date.now()-o<1e4)return;if(t.actionIdentifier!==e.DEFAULT_ACTION_IDENTIFIER&&actionHandler){routedAt.set(n,Date.now());try{await actionHandler(toPushAction(t))}catch(e){Logger.error("[push] onAction threw",e)}}else if(config){routedAt.set(n,Date.now());try{config.onOpen(responseData(t))}catch(e){Logger.error("[push] onOpen threw",e)}}else pendingResponses.push(t)},drainPending=async e=>{const t=pendingResponses.splice(0);for(const n of t)await routeResponse(e,n)},listenForResponses=e=>{listening||(listening=!0,e.addNotificationResponseReceivedListener(t=>{routeResponse(e,t)}),e.getLastNotificationResponseAsync().then(async t=>(t&&await routeResponse(e,t),e.clearLastNotificationResponseAsync?.())).catch(e=>Logger.error("[push] reading the launch notification failed",e)))};export const routeTaskPayload=async e=>{const t=resolveModules(),n=e;t&&"string"==typeof n?.actionIdentifier&&"string"==typeof n.notification?.request?.identifier&&await routeResponse(t.notifications,n)};const createChannels=async(e,t)=>{for(const n of t)try{await e.setNotificationChannelAsync(n.id,{name:n.name,importance:"high"===n.importance?e.AndroidImportance.HIGH:e.AndroidImportance.DEFAULT})}catch(e){Logger.error(`[push] creating channel ${n.id} failed`,e)}},APNS_ENVIRONMENTS={development:"Development",production:"Production"},readApnsEnvironment=async e=>{try{return APNS_ENVIRONMENTS[await e.getApnsEnvironment()??""]??null}catch(e){return Logger.warn("[push] reading the APNs environment failed",e),null}},toDevice=async(e,t)=>{const n="android"===e.type?"Fcm":"ios"===e.type?"Apns":null;if(!n||"string"!=typeof e.data)return null;if(!t.applicationId)throw new Error("[@webority/mobile-core] push needs the app bundle id from expo-application.");return{installationId:await requireConfig().getInstallationId(),platform:n,token:e.data,appId:t.applicationId,apnsEnvironment:"Apns"===n?await readApnsEnvironment(t):null}};export const initPush=e=>{const t=null===config;if(config=e,!t)return;const n=resolveModules();if(!n)return void setState({status:"unavailable"});const o=n.notifications;o.setNotificationHandler({handleNotification:async()=>{const e=config?.showInForeground?.()??!0;return{shouldShowBanner:e,shouldShowList:e,shouldPlaySound:e&&(config?.playSoundInForeground?.()??!0),shouldSetBadge:!0}}}),"android"===Platform.OS&&e.androidChannels?.length&&(channelsReady=createChannels(o,e.androidChannels)),listenForResponses(o),drainPending(o),o.getPermissionsAsync().then(e=>setState({status:toPermissionStatus(e)})).catch(e=>Logger.error("[push] permission check failed",e))};let registering=null,registeredKey=null,unregistering=null,tokenDrop=Promise.resolve();const registerOnce=e=>{const t=JSON.stringify(e);if(registering?.key===t)return registering.promise;const n=requireConfig().register(e).then(()=>{registeredKey=t}).finally(()=>{registering?.promise===n&&(registering=null)});return registering={key:t,promise:n},n};export const enablePush=async({prompt:e=!0}={})=>{requireConfig();const t=resolveModules();if(!t)return setState({status:"unavailable"}),"unavailable";const n=t.notifications;await channelsReady,await tokenDrop;let o=await n.getPermissionsAsync();if(!o.granted&&e&&(o=await n.requestPermissionsAsync()),!o.granted){const e=toPermissionStatus(o);return setState({status:e}),e}const i=await toDevice(await n.getDevicePushTokenAsync(),t);return i?(await registerOnce(i),tokenSubscription?.remove(),tokenSubscription=n.addPushTokenListener(e=>{(unregistering??Promise.resolve()).then(()=>state.enabled?toDevice(e,t):null).then(e=>e&&JSON.stringify(e)!==registeredKey?registerOnce(e):void 0).catch(e=>Logger.error("[push] re-registering a new token failed",e))}),setState({status:"granted",enabled:!0}),"granted"):(setState({status:"unavailable"}),"unavailable")};const stopFollowingToken=()=>{tokenSubscription?.remove(),tokenSubscription=null,registeredKey=null,setState({enabled:!1})};export const disablePush=async()=>{const e=requireConfig(),t=await e.getInstallationId();await(registering?.promise.catch(()=>{}));let settle=()=>{};const n=new Promise(e=>{settle=e});unregistering=n;try{await e.unregister(t),stopFollowingToken()}finally{unregistering===n&&(unregistering=null),settle()}};export const resetPush=async()=>{stopFollowingToken();const e=resolveModules();if(!e)return;const t=e.notifications.unregisterForNotificationsAsync();tokenDrop=t.catch(()=>{}),await t};export const setBadgeCount=async e=>{const t=resolveModules();t&&await t.notifications.setBadgeCountAsync(e)};export const setCategories=async e=>{const t=resolveModules();if(t)for(const n of e)await t.notifications.setNotificationCategoryAsync(n.id,toNotificationActions(n.actions))};export const onAction=e=>{actionHandler=e;const t=resolveModules();return t&&(listenForResponses(t.notifications),drainPending(t.notifications)),()=>{actionHandler===e&&(actionHandler=null)}};export const onReceive=e=>{receiveHandlers.add(e);const t=resolveModules();return t&&!listeningForReceived&&(listeningForReceived=!0,t.notifications.addNotificationReceivedListener(e=>{const t=toPushReceived(e);for(const e of[...receiveHandlers])try{e(t)}catch(e){Logger.error("[push] onReceive threw",e)}})),()=>{receiveHandlers.delete(e)}};export const notifyLocal=async({title:e,body:t,data:n})=>{const o=resolveModules();o&&await o.notifications.scheduleNotificationAsync({content:{title:e,body:t,data:n??{}},trigger:null})};const subscribe=e=>(subscribers.add(e),()=>{subscribers.delete(e)}),getSnapshot=()=>state,enable=async()=>{await enablePush()};export const usePush=()=>({...useSyncExternalStore(subscribe,getSnapshot,getSnapshot),enable:enable,disable:disablePush});
@@ -1 +1 @@
1
- "use strict";import*as ExpoSqlite from"expo-sqlite";import{Logger}from"../logger/index.js";import{adaptExpoSqlite}from"./adapters/expo.js";let expoDriver,backend=null;export const setSqliteImplementation=e=>{backend=e};const hasNativeSqlite=e=>"function"==typeof e?.openDatabaseAsync,resolveDriver=()=>{if(backend)return backend;if(void 0===expoDriver){const e=ExpoSqlite;expoDriver=hasNativeSqlite(e)?adaptExpoSqlite(e):null}return expoDriver};export const isSqliteAvailable=()=>null!==resolveDriver();const requireBackend=e=>{const t=resolveDriver();if(!t){const t=new Error(`[@webority/mobile-core] No SQLite driver available; cannot ${e}. Install expo-sqlite (the only sanctioned driver) in the consuming app.`);throw Logger.error(t.message),t}return t};export const MIGRATIONS_TABLE="_webority_migrations";const IDENTIFIER=/^[A-Za-z_][A-Za-z0-9_]*$/;export const assertSqlIdentifier=(e,t="identifier")=>{if("string"!=typeof e||!IDENTIFIER.test(e))throw new Error(`[@webority/mobile-core] Invalid SQL ${t} ${JSON.stringify(e)}. Only letters, digits and underscores are allowed, and it must not start with a digit. Pass values as bound parameters instead of building them into the name.`);return e};const quoteIdentifier=(e,t="identifier")=>`"${assertSqlIdentifier(e,t)}"`,escapeIdentifier=e=>`"${e.replace(/"/g,'""')}"`,COLUMN_DEFINITION=/^[A-Za-z0-9_ ,.()'"+*/=<>|%:$-]+$/,assertColumnDefinition=(e,t)=>{const r="string"==typeof e?e.trim():"";if(0===r.length||!COLUMN_DEFINITION.test(r)||r.includes("--"))throw new Error(`[@webority/mobile-core] Invalid column definition for "${t}": ${JSON.stringify(e)}. A definition is raw SQL such as 'INTEGER NOT NULL DEFAULT 0' and may not contain a statement separator or a comment.`);return r};export const toSqliteValue=e=>void 0===e?null:"boolean"==typeof e?e?1:0:e instanceof Date?e.toISOString():e;export const fromSqliteBoolean=e=>{if("boolean"==typeof e)return e;if("number"==typeof e)return 0!==e;if("string"==typeof e){const t=e.trim().toLowerCase();return"1"===t||"true"===t}return!1};export const whereIn=(e,t)=>{const r=quoteIdentifier(e,"column name");return 0===t.length?{sql:"1 = 0",params:[]}:{sql:`${r} IN (${t.map(()=>"?").join(", ")})`,params:t.map(toSqliteValue)}};const isRawWhere=e=>"string"==typeof e.sql&&Object.keys(e).every(e=>"sql"===e||"params"===e),buildWhere=e=>{if(!e)return{clause:"",params:[]};if(isRawWhere(e)){const t=e.sql.trim();return 0===t.length?{clause:"",params:[]}:{clause:` WHERE ${t}`,params:[...e.params??[]]}}const t=Object.entries(e);if(0===t.length)return{clause:"",params:[]};const r=[],n=[];for(const[e,i]of t){const t=quoteIdentifier(e,"column name"),o=toSqliteValue(i);null===o?r.push(`${t} IS NULL`):(r.push(`${t} = ?`),n.push(o))}return{clause:` WHERE ${r.join(" AND ")}`,params:n}},buildOrderBy=e=>{if(void 0===e)return"";const t=e.split(",").map(e=>e.trim()).filter(e=>e.length>0);if(0===t.length)return"";return` ORDER BY ${t.map(t=>{const[r,n,...i]=t.split(/\s+/);if(i.length>0)throw new Error(`[@webority/mobile-core] Invalid orderBy term ${JSON.stringify(t)}. Expected a column name optionally followed by ASC or DESC.`);const o=quoteIdentifier(r,"orderBy column");if(void 0===n)return o;const a=n.toUpperCase();if("ASC"!==a&&"DESC"!==a)throw new Error(`[@webority/mobile-core] Invalid sort direction ${JSON.stringify(n)} in orderBy ${JSON.stringify(e)}. Expected ASC or DESC.`);return`${o} ${a}`}).join(", ")}`},assertNonNegativeInteger=(e,t)=>{if(!Number.isInteger(e)||e<0)throw new Error(`[@webority/mobile-core] Invalid ${t} ${JSON.stringify(e)}. Expected a non-negative integer.`);return e},hashString=e=>{let t=2166136261;for(let r=0;r<e.length;r+=1)t^=e.charCodeAt(r),t=Math.imul(t,16777619)>>>0;return`${t.toString(16).padStart(8,"0")}-${e.length.toString(16)}`},migrationChecksum=e=>{if("function"==typeof e.up)return hashString("fn");const t=Array.isArray(e.up)?e.up.join("\n"):e.up;return hashString(t)};export const DEFAULT_PRAGMAS={journal_mode:"WAL",synchronous:"NORMAL",foreign_keys:"ON",busy_timeout:5e3};const PRAGMA_VALUE=/^[A-Za-z0-9_-]+$/,renderPragmaValue=(e,t)=>{if("number"==typeof t){if(!Number.isFinite(t))throw new Error(`[@webority/mobile-core] Invalid value for PRAGMA ${e}: ${t}.`);return String(t)}if(!PRAGMA_VALUE.test(t))throw new Error(`[@webority/mobile-core] Invalid value ${JSON.stringify(t)} for PRAGMA ${e}. PRAGMA values cannot be bound, so only letters, digits, underscores and hyphens are accepted.`);return t},createDb=(e,t)=>{let r=Promise.resolve();const execute=async(e,r)=>{try{return await t.execute(e,r)}catch(t){throw Logger.error(`[@webority/mobile-core] SQLite execute failed: ${e}`,t),t}},select=async(e,t)=>(await execute(e,t)).rows,selectOne=async(e,t)=>{const r=await select(e,t);return r.length>0?r[0]:null},runTransaction=async e=>{let r;await t.execute("BEGIN");try{r=await e({execute:execute,select:select,selectOne:selectOne})}catch(e){try{await t.execute("ROLLBACK")}catch(e){Logger.error("[@webority/mobile-core] SQLite ROLLBACK failed",e)}throw e}try{await t.execute("COMMIT")}catch(e){try{await t.execute("ROLLBACK")}catch{}throw e}return r},transaction=e=>{const t=r.then(()=>runTransaction(e),()=>runTransaction(e));return r=t.catch(()=>{}),t},insertSql=(e,t)=>{const r=quoteIdentifier(e,"table name"),n=Object.entries(t);if(0===n.length)throw new Error(`[@webority/mobile-core] Cannot insert an empty row into ${e}. Pass at least one column.`);const i=n.map(([e])=>quoteIdentifier(e,"column name")),o=n.map(()=>"?");return{sql:`INSERT INTO ${r} (${i.join(", ")}) VALUES (${o.join(", ")})`,params:n.map(([,e])=>toSqliteValue(e))}},findAll=async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=buildWhere(t),o=[...i.params];let a=`SELECT * FROM ${n}${i.clause}${buildOrderBy(r?.orderBy)}`;return void 0!==r?.limit?(assertNonNegativeInteger(r.limit,"limit"),a+=" LIMIT ?",o.push(r.limit)):void 0!==r?.offset&&(a+=" LIMIT -1"),void 0!==r?.offset&&(assertNonNegativeInteger(r.offset,"offset"),a+=" OFFSET ?",o.push(r.offset)),select(a,o)},ensureLedger=async()=>{await execute(`CREATE TABLE IF NOT EXISTS ${escapeIdentifier(MIGRATIONS_TABLE)} (version INTEGER PRIMARY KEY, name TEXT, applied_at TEXT NOT NULL, checksum TEXT NOT NULL)`)},n=`INSERT OR REPLACE INTO ${escapeIdentifier(MIGRATIONS_TABLE)} (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)`,wipe=async()=>{const e=await selectOne("PRAGMA foreign_keys"),t=1===Number(e?.foreign_keys??0);await execute("PRAGMA foreign_keys = OFF");try{await transaction(async e=>{const t=await e.select("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name <> ?",[MIGRATIONS_TABLE]);for(const r of t)await e.execute(`DELETE FROM ${escapeIdentifier(r.name)}`)})}finally{await execute("PRAGMA foreign_keys = "+(t?"ON":"OFF"))}},migrate=async(t,r)=>{const i=new Set;for(const r of t){if(i.has(r.version))throw new Error(`[@webority/mobile-core] Duplicate migration version ${r.version} for database "${e}".`);i.add(r.version)}const o=await selectOne("PRAGMA user_version");let a=Number(o?.user_version??0);if(t.length>0&&!r?.allowDowngrade){const r=t.reduce((e,t)=>Math.max(e,t.version),0);if(a>r)throw new Error(`[@webority/mobile-core] Database "${e}" is at schema version ${a}, which is newer than the highest migration supplied (${r}). This build would run old code against a newer schema. Ship the missing migrations, or pass { allowDowngrade: true } if this build is known to be compatible.`)}await ensureLedger();const s=await select(`SELECT version, checksum FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),c=new Map(s.map(e=>[Number(e.version),String(e.checksum)]));for(const r of t){if(r.version>a)continue;const t=c.get(r.version);if(void 0===t)continue;const n=migrationChecksum(r);if(t!==n)throw new Error(`[@webority/mobile-core] Migration ${r.version} has changed since it was applied to database "${e}" (recorded ${t}, now ${n}). A released migration must never be edited; add a new one instead.`)}const l=t.filter(e=>e.version>a).sort((e,t)=>e.version-t.version);for(const e of l){const t=migrationChecksum(e);await transaction(async r=>{if("function"==typeof e.up)await e.up(r);else{const t=Array.isArray(e.up)?e.up:[e.up];for(const e of t)await r.execute(e)}await r.execute(n,[e.version,e.name??null,(new Date).toISOString(),t]),await r.execute(`PRAGMA user_version = ${e.version}`)}),a=e.version}return a};return{name:e,execute:execute,select:select,selectOne:selectOne,transaction:transaction,insert:async(e,t)=>{const{sql:r,params:n}=insertSql(e,t);return(await execute(r,n)).insertId},insertMany:async(e,t)=>{if(0===t.length)return 0;const r=t.map(t=>insertSql(e,t));return await transaction(async e=>{for(const t of r)await e.execute(t.sql,t.params)}),t.length},update:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=Object.entries(t);if(0===i.length)throw new Error(`[@webority/mobile-core] Cannot update ${e} with no values. Pass at least one column to set.`);const o=i.map(([e])=>`${quoteIdentifier(e,"column name")} = ?`),a=i.map(([,e])=>toSqliteValue(e)),s=buildWhere(r);return(await execute(`UPDATE ${n} SET ${o.join(", ")}${s.clause}`,[...a,...s.params])).rowsAffected},remove:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t);return(await execute(`DELETE FROM ${r}${n.clause}`,n.params)).rowsAffected},upsert:async(e,t,r)=>{if(0===r.length)throw new Error(`[@webority/mobile-core] upsert on ${e} needs at least one conflict column; without one SQLite has no key to match an existing row on.`);const{sql:n,params:i}=insertSql(e,t),o=new Set(r),a=r.map(e=>quoteIdentifier(e,"column name")),s=Object.keys(t).filter(e=>!o.has(e)).map(e=>{const t=quoteIdentifier(e,"column name");return`${t} = excluded.${t}`}),c=s.length>0?`DO UPDATE SET ${s.join(", ")}`:"DO NOTHING";await execute(`${n} ON CONFLICT (${a.join(", ")}) ${c}`,i)},findAll:findAll,findOne:async(e,t)=>{const r=await findAll(e,t,{limit:1});return r.length>0?r[0]:null},count:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t),i=await selectOne(`SELECT COUNT(*) AS cnt FROM ${r}${n.clause}`,n.params);return Number(i?.cnt??0)},tableExists:async e=>{assertSqlIdentifier(e,"table name");return null!==await selectOne("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",[e])},createTable:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=Object.entries(t);if(0===i.length)throw new Error(`[@webority/mobile-core] Cannot create table ${e} with no columns.`);const o=i.map(([e,t])=>`${quoteIdentifier(e,"column name")} ${assertColumnDefinition(t,e)}`),a=!1===r?.ifNotExists?"":"IF NOT EXISTS ";await execute(`CREATE TABLE ${a}${n} (${o.join(", ")})`)},dropTable:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=!1===t?.ifExists?"":"IF EXISTS ";await execute(`DROP TABLE ${n}${r}`)},wipe:wipe,reset:async e=>(await wipe(),await ensureLedger(),await transaction(async e=>{await e.execute(`DELETE FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),await e.execute("PRAGMA user_version = 0")}),migrate(e)),migrate:migrate,baseline:async(e,t)=>{assertNonNegativeInteger(e,"baseline version"),await ensureLedger();const r=(new Date).toISOString();await transaction(async i=>{for(const o of t??[])o.version>e||await i.execute(n,[o.version,o.name??null,r,migrationChecksum(o)]);await i.execute(`PRAGMA user_version = ${e}`)})},close:async()=>{await t.close()}}};export const openDatabase=async(e,t)=>{const r=requireBackend("open"),n=await r.open(e,t?{location:t.location}:void 0),i=createDb(e,n);if(!1!==t?.pragmas){const e={...DEFAULT_PRAGMAS,...t?.pragmas??{}};for(const[t,r]of Object.entries(e))assertSqlIdentifier(t,"PRAGMA name"),await i.execute(`PRAGMA ${t} = ${renderPragmaValue(t,r)}`)}return i};
1
+ "use strict";import*as ExpoSqlite from"expo-sqlite";import{Logger}from"../logger/index.js";import{adaptExpoSqlite}from"./adapters/expo.js";let expoDriver,backend=null;export const setSqliteImplementation=e=>{backend=e};const hasNativeSqlite=e=>"function"==typeof e?.openDatabaseAsync,resolveDriver=()=>{if(backend)return backend;if(void 0===expoDriver){const e=ExpoSqlite;expoDriver=hasNativeSqlite(e)?adaptExpoSqlite(e):null}return expoDriver};export const isSqliteAvailable=()=>null!==resolveDriver();const requireBackend=e=>{const t=resolveDriver();if(!t){const t=new Error(`[@webority/mobile-core] No SQLite driver available; cannot ${e}. Install expo-sqlite (the only sanctioned driver) in the consuming app.`);throw Logger.error(t.message),t}return t};export const MIGRATIONS_TABLE="_webority_migrations";const IDENTIFIER=/^[A-Za-z_][A-Za-z0-9_]*$/;export const assertSqlIdentifier=(e,t="identifier")=>{if("string"!=typeof e||!IDENTIFIER.test(e))throw new Error(`[@webority/mobile-core] Invalid SQL ${t} ${JSON.stringify(e)}. Only letters, digits and underscores are allowed, and it must not start with a digit. Pass values as bound parameters instead of building them into the name.`);return e};const quoteIdentifier=(e,t="identifier")=>`"${assertSqlIdentifier(e,t)}"`,escapeIdentifier=e=>`"${e.replace(/"/g,'""')}"`,COLUMN_DEFINITION=/^[A-Za-z0-9_ ,.()'"+*/=<>|%:$-]+$/,assertColumnDefinition=(e,t)=>{const r="string"==typeof e?e.trim():"";if(0===r.length||!COLUMN_DEFINITION.test(r)||r.includes("--"))throw new Error(`[@webority/mobile-core] Invalid column definition for "${t}": ${JSON.stringify(e)}. A definition is raw SQL such as 'INTEGER NOT NULL DEFAULT 0' and may not contain a statement separator or a comment.`);return r};export const toSqliteValue=e=>void 0===e?null:"boolean"==typeof e?e?1:0:e instanceof Date?e.toISOString():e;export const fromSqliteBoolean=e=>{if("boolean"==typeof e)return e;if("number"==typeof e)return 0!==e;if("string"==typeof e){const t=e.trim().toLowerCase();return"1"===t||"true"===t}return!1};export const whereIn=(e,t)=>{const r=quoteIdentifier(e,"column name");return 0===t.length?{sql:"1 = 0",params:[]}:{sql:`${r} IN (${t.map(()=>"?").join(", ")})`,params:t.map(toSqliteValue)}};const isRawWhere=e=>"string"==typeof e.sql&&Object.keys(e).every(e=>"sql"===e||"params"===e),buildWhere=e=>{if(!e)return{clause:"",params:[]};if(isRawWhere(e)){const t=e.sql.trim();return 0===t.length?{clause:"",params:[]}:{clause:` WHERE ${t}`,params:[...e.params??[]]}}const t=Object.entries(e);if(0===t.length)return{clause:"",params:[]};const r=[],n=[];for(const[e,i]of t){const t=quoteIdentifier(e,"column name"),o=toSqliteValue(i);null===o?r.push(`${t} IS NULL`):(r.push(`${t} = ?`),n.push(o))}return{clause:` WHERE ${r.join(" AND ")}`,params:n}},buildOrderBy=e=>{if(void 0===e)return"";const t=e.split(",").map(e=>e.trim()).filter(e=>e.length>0);if(0===t.length)return"";return` ORDER BY ${t.map(t=>{const[r,n,...i]=t.split(/\s+/);if(i.length>0)throw new Error(`[@webority/mobile-core] Invalid orderBy term ${JSON.stringify(t)}. Expected a column name optionally followed by ASC or DESC.`);const o=quoteIdentifier(r,"orderBy column");if(void 0===n)return o;const a=n.toUpperCase();if("ASC"!==a&&"DESC"!==a)throw new Error(`[@webority/mobile-core] Invalid sort direction ${JSON.stringify(n)} in orderBy ${JSON.stringify(e)}. Expected ASC or DESC.`);return`${o} ${a}`}).join(", ")}`},assertNonNegativeInteger=(e,t)=>{if(!Number.isInteger(e)||e<0)throw new Error(`[@webority/mobile-core] Invalid ${t} ${JSON.stringify(e)}. Expected a non-negative integer.`);return e},hashString=e=>{let t=2166136261;for(let r=0;r<e.length;r+=1)t^=e.charCodeAt(r),t=Math.imul(t,16777619)>>>0;return`${t.toString(16).padStart(8,"0")}-${e.length.toString(16)}`},migrationChecksum=e=>{if("function"==typeof e.up)return hashString("fn");const t=Array.isArray(e.up)?e.up.join("\n"):e.up;return hashString(t)};export const DEFAULT_PRAGMAS={journal_mode:"WAL",synchronous:"NORMAL",foreign_keys:"ON",busy_timeout:5e3};const PRAGMA_VALUE=/^[A-Za-z0-9_-]+$/,renderPragmaValue=(e,t)=>{if("number"==typeof t){if(!Number.isFinite(t))throw new Error(`[@webority/mobile-core] Invalid value for PRAGMA ${e}: ${t}.`);return String(t)}if(!PRAGMA_VALUE.test(t))throw new Error(`[@webority/mobile-core] Invalid value ${JSON.stringify(t)} for PRAGMA ${e}. PRAGMA values cannot be bound, so only letters, digits, underscores and hyphens are accepted.`);return t},queriesOver=e=>{const select=async(t,r)=>(await e(t,r)).rows;return{execute:e,select:select,selectOne:async(e,t)=>{const r=await select(e,t);return r.length>0?r[0]:null}}},createDb=(e,t)=>{let r=Promise.resolve();const exclusive=e=>{const t=r.then(e);return r=t.catch(()=>{}),t},run=async(e,r)=>{try{return await t.execute(e,r)}catch(t){throw Logger.error(`[@webority/mobile-core] SQLite execute failed: ${e}`,t),t}},n=queriesOver(run),{execute:i,select:o,selectOne:a}=queriesOver((e,t)=>exclusive(()=>run(e,t))),runTransaction=async e=>{let r=!0;const n=queriesOver((e,t)=>r?run(e,t):Promise.reject(new Error("[@webority/mobile-core] A transaction handle was used after its transaction ended. Use the database itself outside transaction().")));try{let r;await t.execute("BEGIN");try{r=await e(n)}catch(e){try{await t.execute("ROLLBACK")}catch(e){Logger.error("[@webority/mobile-core] SQLite ROLLBACK failed",e)}throw e}try{await t.execute("COMMIT")}catch(e){try{await t.execute("ROLLBACK")}catch{}throw e}return r}finally{r=!1}},transaction=e=>exclusive(()=>runTransaction(e)),insertSql=(e,t)=>{const r=quoteIdentifier(e,"table name"),n=Object.entries(t);if(0===n.length)throw new Error(`[@webority/mobile-core] Cannot insert an empty row into ${e}. Pass at least one column.`);const i=n.map(([e])=>quoteIdentifier(e,"column name")),o=n.map(()=>"?");return{sql:`INSERT INTO ${r} (${i.join(", ")}) VALUES (${o.join(", ")})`,params:n.map(([,e])=>toSqliteValue(e))}},findAll=async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),i=buildWhere(t),a=[...i.params];let s=`SELECT * FROM ${n}${i.clause}${buildOrderBy(r?.orderBy)}`;return void 0!==r?.limit?(assertNonNegativeInteger(r.limit,"limit"),s+=" LIMIT ?",a.push(r.limit)):void 0!==r?.offset&&(s+=" LIMIT -1"),void 0!==r?.offset&&(assertNonNegativeInteger(r.offset,"offset"),s+=" OFFSET ?",a.push(r.offset)),o(s,a)},ensureLedger=async()=>{await i(`CREATE TABLE IF NOT EXISTS ${escapeIdentifier(MIGRATIONS_TABLE)} (version INTEGER PRIMARY KEY, name TEXT, applied_at TEXT NOT NULL, checksum TEXT NOT NULL)`)},s=`INSERT OR REPLACE INTO ${escapeIdentifier(MIGRATIONS_TABLE)} (version, name, applied_at, checksum) VALUES (?, ?, ?, ?)`,wipe=()=>exclusive(async()=>{const e=await n.selectOne("PRAGMA foreign_keys"),t=1===Number(e?.foreign_keys??0);await run("PRAGMA foreign_keys = OFF");try{await runTransaction(async e=>{const t=await e.select("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name <> ?",[MIGRATIONS_TABLE]);for(const r of t)await e.execute(`DELETE FROM ${escapeIdentifier(r.name)}`)})}finally{await run("PRAGMA foreign_keys = "+(t?"ON":"OFF"))}}),migrate=async(t,r)=>{const n=new Set;for(const r of t){if(n.has(r.version))throw new Error(`[@webority/mobile-core] Duplicate migration version ${r.version} for database "${e}".`);n.add(r.version)}const i=await a("PRAGMA user_version");let c=Number(i?.user_version??0);if(t.length>0&&!r?.allowDowngrade){const r=t.reduce((e,t)=>Math.max(e,t.version),0);if(c>r)throw new Error(`[@webority/mobile-core] Database "${e}" is at schema version ${c}, which is newer than the highest migration supplied (${r}). This build would run old code against a newer schema. Ship the missing migrations, or pass { allowDowngrade: true } if this build is known to be compatible.`)}await ensureLedger();const l=await o(`SELECT version, checksum FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),u=new Map(l.map(e=>[Number(e.version),String(e.checksum)]));for(const r of t){if(r.version>c)continue;const t=u.get(r.version);if(void 0===t)continue;const n=migrationChecksum(r);if(t!==n)throw new Error(`[@webority/mobile-core] Migration ${r.version} has changed since it was applied to database "${e}" (recorded ${t}, now ${n}). A released migration must never be edited; add a new one instead.`)}const m=t.filter(e=>e.version>c).sort((e,t)=>e.version-t.version);for(const e of m){const t=migrationChecksum(e);await transaction(async r=>{if("function"==typeof e.up)await e.up(r);else{const t=Array.isArray(e.up)?e.up:[e.up];for(const e of t)await r.execute(e)}await r.execute(s,[e.version,e.name??null,(new Date).toISOString(),t]),await r.execute(`PRAGMA user_version = ${e.version}`)}),c=e.version}return c};return{name:e,execute:i,select:o,selectOne:a,transaction:transaction,insert:async(e,t)=>{const{sql:r,params:n}=insertSql(e,t);return(await i(r,n)).insertId},insertMany:async(e,t)=>{if(0===t.length)return 0;const r=t.map(t=>insertSql(e,t));return await transaction(async e=>{for(const t of r)await e.execute(t.sql,t.params)}),t.length},update:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),o=Object.entries(t);if(0===o.length)throw new Error(`[@webority/mobile-core] Cannot update ${e} with no values. Pass at least one column to set.`);const a=o.map(([e])=>`${quoteIdentifier(e,"column name")} = ?`),s=o.map(([,e])=>toSqliteValue(e)),c=buildWhere(r);return(await i(`UPDATE ${n} SET ${a.join(", ")}${c.clause}`,[...s,...c.params])).rowsAffected},remove:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t);return(await i(`DELETE FROM ${r}${n.clause}`,n.params)).rowsAffected},upsert:async(e,t,r)=>{if(0===r.length)throw new Error(`[@webority/mobile-core] upsert on ${e} needs at least one conflict column; without one SQLite has no key to match an existing row on.`);const{sql:n,params:o}=insertSql(e,t),a=new Set(r),s=r.map(e=>quoteIdentifier(e,"column name")),c=Object.keys(t).filter(e=>!a.has(e)).map(e=>{const t=quoteIdentifier(e,"column name");return`${t} = excluded.${t}`}),l=c.length>0?`DO UPDATE SET ${c.join(", ")}`:"DO NOTHING";await i(`${n} ON CONFLICT (${s.join(", ")}) ${l}`,o)},findAll:findAll,findOne:async(e,t)=>{const r=await findAll(e,t,{limit:1});return r.length>0?r[0]:null},count:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=buildWhere(t),i=await a(`SELECT COUNT(*) AS cnt FROM ${r}${n.clause}`,n.params);return Number(i?.cnt??0)},tableExists:async e=>{assertSqlIdentifier(e,"table name");return null!==await a("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",[e])},createTable:async(e,t,r)=>{const n=quoteIdentifier(e,"table name"),o=Object.entries(t);if(0===o.length)throw new Error(`[@webority/mobile-core] Cannot create table ${e} with no columns.`);const a=o.map(([e,t])=>`${quoteIdentifier(e,"column name")} ${assertColumnDefinition(t,e)}`),s=!1===r?.ifNotExists?"":"IF NOT EXISTS ";await i(`CREATE TABLE ${s}${n} (${a.join(", ")})`)},dropTable:async(e,t)=>{const r=quoteIdentifier(e,"table name"),n=!1===t?.ifExists?"":"IF EXISTS ";await i(`DROP TABLE ${n}${r}`)},wipe:wipe,reset:async e=>(await wipe(),await ensureLedger(),await transaction(async e=>{await e.execute(`DELETE FROM ${escapeIdentifier(MIGRATIONS_TABLE)}`),await e.execute("PRAGMA user_version = 0")}),migrate(e)),migrate:migrate,baseline:async(e,t)=>{assertNonNegativeInteger(e,"baseline version"),await ensureLedger();const r=(new Date).toISOString();await transaction(async n=>{for(const i of t??[])i.version>e||await n.execute(s,[i.version,i.name??null,r,migrationChecksum(i)]);await n.execute(`PRAGMA user_version = ${e}`)})},close:()=>exclusive(()=>t.close())}};export const openDatabase=async(e,t)=>{const r=requireBackend("open"),n=await r.open(e,t?{location:t.location}:void 0),i=createDb(e,n);if(!1!==t?.pragmas){const e={...DEFAULT_PRAGMAS,...t?.pragmas??{}};for(const[t,r]of Object.entries(e))assertSqlIdentifier(t,"PRAGMA name"),await i.execute(`PRAGMA ${t} = ${renderPragmaValue(t,r)}`)}return i};
@@ -1,5 +1,5 @@
1
- export type { PushAction, PushActionHandler, PushAndroidChannel, PushCategory, PushCategoryAction, PushConfig, PushDevice, PushLocalNotification, PushModules, PushNotificationsLike, PushPlatform, PushState } from './push';
2
- export { disablePush, enablePush, initPush, notifyLocal, onAction, setBadgeCount, setCategories, setPushImplementation, usePush } from './push';
1
+ export type { EnablePushOptions, PushAction, PushActionHandler, PushAndroidChannel, PushApnsEnvironment, PushCategory, PushCategoryAction, PushConfig, PushDevice, PushLocalNotification, PushModules, PushNotificationsLike, PushPlatform, PushReceived, PushReceiveHandler, PushState } from './push';
2
+ export { disablePush, enablePush, initPush, notifyLocal, onAction, onReceive, resetPush, setBadgeCount, setCategories, setPushImplementation, usePush } from './push';
3
3
  export type { PushStatus } from './status';
4
4
  export { PUSH_STATUS } from './status';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,11 +1,37 @@
1
1
  import type { PushStatus } from './status';
2
2
  export type PushPlatform = 'Fcm' | 'Apns';
3
+ /** Which APNs gateway accepts this build's token: the sandbox for development signing, else production. */
4
+ export type PushApnsEnvironment = 'Development' | 'Production';
3
5
  export interface PushDevice {
4
6
  installationId: string;
5
7
  platform: PushPlatform;
6
8
  token: string;
7
9
  /** The app's bundle id, the APNs topic on iOS. */
8
10
  appId: string;
11
+ /**
12
+ * The APNs gateway this build's signing entitles, read from the app's
13
+ * `aps-environment`. Null on Android, and on iOS when it cannot be read
14
+ * (the simulator), so the server falls back to its own setting.
15
+ */
16
+ apnsEnvironment: PushApnsEnvironment | null;
17
+ }
18
+ /** A notification that arrived while the app was open. */
19
+ export interface PushReceived {
20
+ /** The notification's identifier on the phone, not a server id in `data`. */
21
+ notificationId: string;
22
+ title: string | null;
23
+ body: string | null;
24
+ data: Record<string, string>;
25
+ }
26
+ export type PushReceiveHandler = (notification: PushReceived) => void;
27
+ export interface EnablePushOptions {
28
+ /**
29
+ * Whether to show the system permission prompt when permission has not been
30
+ * granted yet. Defaults to true. With false, a phone already granted is
31
+ * registered silently and any other phone is left alone, so the app can ask
32
+ * later at a moment it chooses.
33
+ */
34
+ prompt?: boolean;
9
35
  }
10
36
  export interface PushAndroidChannel {
11
37
  id: string;
@@ -65,7 +91,7 @@ export interface PushLocalNotification {
65
91
  }
66
92
  export interface PushState {
67
93
  status: PushStatus;
68
- /** True once this process has registered the device, false after `disablePush`. */
94
+ /** True once this process has registered the device, false after `disablePush` or `resetPush`. */
69
95
  enabled: boolean;
70
96
  }
71
97
  interface PermissionResponseLike {
@@ -75,28 +101,31 @@ interface PermissionResponseLike {
75
101
  interface SubscriptionLike {
76
102
  remove: () => void;
77
103
  }
78
- interface NotificationResponseLike {
79
- actionIdentifier: string;
80
- userText?: string;
81
- notification: {
82
- request: {
83
- identifier: string;
84
- /** A background task receives the raw content, where data may still be a JSON `dataString`. */
85
- content: {
104
+ interface NotificationLike {
105
+ request: {
106
+ identifier: string;
107
+ /** A background task receives the raw content, where data may still be a JSON `dataString`. */
108
+ content: {
109
+ title?: string | null;
110
+ body?: string | null;
111
+ data?: Record<string, unknown> | null;
112
+ dataString?: string;
113
+ };
114
+ /** A remote push carries its raw payload: APNs `userInfo` on iOS, the FCM message on Android. */
115
+ trigger?: {
116
+ type?: string;
117
+ payload?: Record<string, unknown> | null;
118
+ remoteMessage?: {
86
119
  data?: Record<string, unknown> | null;
87
- dataString?: string;
88
- };
89
- /** A remote push carries its raw payload: APNs `userInfo` on iOS, the FCM message on Android. */
90
- trigger?: {
91
- type?: string;
92
- payload?: Record<string, unknown> | null;
93
- remoteMessage?: {
94
- data?: Record<string, unknown> | null;
95
- } | null;
96
120
  } | null;
97
- };
121
+ } | null;
98
122
  };
99
123
  }
124
+ interface NotificationResponseLike {
125
+ actionIdentifier: string;
126
+ userText?: string;
127
+ notification: NotificationLike;
128
+ }
100
129
  interface NotificationActionLike {
101
130
  identifier: string;
102
131
  buttonTitle: string;
@@ -119,8 +148,10 @@ export interface PushNotificationsLike {
119
148
  getPermissionsAsync: () => Promise<PermissionResponseLike>;
120
149
  requestPermissionsAsync: () => Promise<PermissionResponseLike>;
121
150
  getDevicePushTokenAsync: () => Promise<DevicePushTokenLike>;
151
+ unregisterForNotificationsAsync: () => Promise<void>;
122
152
  addPushTokenListener: (listener: (token: DevicePushTokenLike) => void) => SubscriptionLike;
123
153
  addNotificationResponseReceivedListener: (listener: (response: NotificationResponseLike) => void) => SubscriptionLike;
154
+ addNotificationReceivedListener: (listener: (notification: NotificationLike) => void) => SubscriptionLike;
124
155
  getLastNotificationResponseAsync: () => Promise<NotificationResponseLike | null>;
125
156
  clearLastNotificationResponseAsync?: () => Promise<void>;
126
157
  setNotificationHandler: (handler: {
@@ -157,6 +188,8 @@ export interface PushModules {
157
188
  notifications: PushNotificationsLike;
158
189
  /** The bundle id, from expo-application. */
159
190
  applicationId: string | null;
191
+ /** expo-application's `getIosPushNotificationServiceEnvironmentAsync`; iOS only. */
192
+ getApnsEnvironment: () => Promise<'development' | 'production' | null>;
160
193
  }
161
194
  /**
162
195
  * Replace the native modules. For tests and the showcase, to reach paths a
@@ -170,6 +203,7 @@ export declare const setPushImplementation: (impl: PushModules | null) => void;
170
203
  * import throws on Android. Exported for `./task`; not on the `/push` entry.
171
204
  */
172
205
  export declare const resolveModules: () => PushModules | null;
206
+ export declare const toPushReceived: (notification: NotificationLike) => PushReceived;
173
207
  export declare const toPushAction: (response: NotificationResponseLike) => PushAction;
174
208
  export declare const toNotificationActions: (actions: PushCategoryAction[]) => NotificationActionLike[];
175
209
  /** The payload of the background notification task: routes button taps, ignores received pushes. */
@@ -183,13 +217,27 @@ export declare const initPush: (next: PushConfig) => void;
183
217
  /**
184
218
  * Asks for permission, reads the native device token and registers it through
185
219
  * `config.register`, then re-registers whenever the OS rotates the token.
220
+ * With `{ prompt: false }` it never shows the permission prompt: a phone
221
+ * already granted registers, and any other resolves with its status
222
+ * (`unknown` when the person has not been asked yet, `denied` when refused).
186
223
  */
187
- export declare const enablePush: () => Promise<"granted" | "denied" | "unavailable">;
224
+ export declare const enablePush: ({ prompt }?: EnablePushOptions) => Promise<PushStatus>;
188
225
  /**
189
- * Unregisters the device through `config.unregister` and stops following token
190
- * changes. Taps on notifications already delivered still reach `onOpen`.
226
+ * Unregisters the device through `config.unregister`, then stops following
227
+ * token changes. When the server call fails it rejects and push stays on, the
228
+ * token listener included, so the app can keep the person's choice and retry.
229
+ * Taps on notifications already delivered still reach `onOpen`.
191
230
  */
192
231
  export declare const disablePush: () => Promise<void>;
232
+ /**
233
+ * Turns push off on this phone alone, with no server call: stops following
234
+ * token changes and drops the native device token, so the server's next send to
235
+ * it fails and the server can delete it. For a sign-out where `disablePush`
236
+ * cannot reach the server, or has no session left to authorise the call. Clears
237
+ * the local state at once; the returned promise settles when the token is gone.
238
+ * `enablePush` waits for the drop before it reads a new token.
239
+ */
240
+ export declare const resetPush: () => Promise<void>;
193
241
  /** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
194
242
  export declare const setBadgeCount: (count: number) => Promise<void>;
195
243
  /**
@@ -204,6 +252,13 @@ export declare const setCategories: (categories: PushCategory[]) => Promise<void
204
252
  * `/push/task`, which sets this handler too. Returns a function that removes it.
205
253
  */
206
254
  export declare const onAction: (handler: PushActionHandler) => (() => void);
255
+ /**
256
+ * Calls `handler` for each notification that arrives while the app is open,
257
+ * whether or not it is shown, so the app can refresh what it displays. Not
258
+ * called for a notification that arrives in the background; its tap still
259
+ * goes to `onOpen`. Returns a function that removes the handler.
260
+ */
261
+ export declare const onReceive: (handler: PushReceiveHandler) => (() => void);
207
262
  /** Shows a notification from the phone itself, for example when a button's request failed. */
208
263
  export declare const notifyLocal: ({ title, body, data }: PushLocalNotification) => Promise<void>;
209
264
  /** Push status for a settings switch, re-rendering when it changes. */
@@ -87,6 +87,11 @@ export interface Db {
87
87
  execute: (sql: string, params?: readonly unknown[]) => Promise<SqliteResultSet>;
88
88
  select: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Promise<T[]>;
89
89
  selectOne: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Promise<T | null>;
90
+ /**
91
+ * Runs `work` alone on the connection: every other call on this database
92
+ * waits until it commits or rolls back. Inside `work` use only `tx`; a call
93
+ * on the database itself would wait for this transaction and never finish.
94
+ */
90
95
  transaction: <T>(work: (tx: Pick<Db, 'execute' | 'select' | 'selectOne'>) => Promise<T>) => Promise<T>;
91
96
  insert: (table: string, row: Record<string, unknown>) => Promise<number | undefined>;
92
97
  insertMany: (table: string, rows: Record<string, unknown>[]) => Promise<number>;
@@ -1,5 +1,5 @@
1
- export type { PushAction, PushActionHandler, PushAndroidChannel, PushCategory, PushCategoryAction, PushConfig, PushDevice, PushLocalNotification, PushModules, PushNotificationsLike, PushPlatform, PushState } from './push.js';
2
- export { disablePush, enablePush, initPush, notifyLocal, onAction, setBadgeCount, setCategories, setPushImplementation, usePush } from './push.js';
1
+ export type { EnablePushOptions, PushAction, PushActionHandler, PushAndroidChannel, PushApnsEnvironment, PushCategory, PushCategoryAction, PushConfig, PushDevice, PushLocalNotification, PushModules, PushNotificationsLike, PushPlatform, PushReceived, PushReceiveHandler, PushState } from './push.js';
2
+ export { disablePush, enablePush, initPush, notifyLocal, onAction, onReceive, resetPush, setBadgeCount, setCategories, setPushImplementation, usePush } from './push.js';
3
3
  export type { PushStatus } from './status.js';
4
4
  export { PUSH_STATUS } from './status.js';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,11 +1,37 @@
1
1
  import type { PushStatus } from './status.js';
2
2
  export type PushPlatform = 'Fcm' | 'Apns';
3
+ /** Which APNs gateway accepts this build's token: the sandbox for development signing, else production. */
4
+ export type PushApnsEnvironment = 'Development' | 'Production';
3
5
  export interface PushDevice {
4
6
  installationId: string;
5
7
  platform: PushPlatform;
6
8
  token: string;
7
9
  /** The app's bundle id, the APNs topic on iOS. */
8
10
  appId: string;
11
+ /**
12
+ * The APNs gateway this build's signing entitles, read from the app's
13
+ * `aps-environment`. Null on Android, and on iOS when it cannot be read
14
+ * (the simulator), so the server falls back to its own setting.
15
+ */
16
+ apnsEnvironment: PushApnsEnvironment | null;
17
+ }
18
+ /** A notification that arrived while the app was open. */
19
+ export interface PushReceived {
20
+ /** The notification's identifier on the phone, not a server id in `data`. */
21
+ notificationId: string;
22
+ title: string | null;
23
+ body: string | null;
24
+ data: Record<string, string>;
25
+ }
26
+ export type PushReceiveHandler = (notification: PushReceived) => void;
27
+ export interface EnablePushOptions {
28
+ /**
29
+ * Whether to show the system permission prompt when permission has not been
30
+ * granted yet. Defaults to true. With false, a phone already granted is
31
+ * registered silently and any other phone is left alone, so the app can ask
32
+ * later at a moment it chooses.
33
+ */
34
+ prompt?: boolean;
9
35
  }
10
36
  export interface PushAndroidChannel {
11
37
  id: string;
@@ -65,7 +91,7 @@ export interface PushLocalNotification {
65
91
  }
66
92
  export interface PushState {
67
93
  status: PushStatus;
68
- /** True once this process has registered the device, false after `disablePush`. */
94
+ /** True once this process has registered the device, false after `disablePush` or `resetPush`. */
69
95
  enabled: boolean;
70
96
  }
71
97
  interface PermissionResponseLike {
@@ -75,28 +101,31 @@ interface PermissionResponseLike {
75
101
  interface SubscriptionLike {
76
102
  remove: () => void;
77
103
  }
78
- interface NotificationResponseLike {
79
- actionIdentifier: string;
80
- userText?: string;
81
- notification: {
82
- request: {
83
- identifier: string;
84
- /** A background task receives the raw content, where data may still be a JSON `dataString`. */
85
- content: {
104
+ interface NotificationLike {
105
+ request: {
106
+ identifier: string;
107
+ /** A background task receives the raw content, where data may still be a JSON `dataString`. */
108
+ content: {
109
+ title?: string | null;
110
+ body?: string | null;
111
+ data?: Record<string, unknown> | null;
112
+ dataString?: string;
113
+ };
114
+ /** A remote push carries its raw payload: APNs `userInfo` on iOS, the FCM message on Android. */
115
+ trigger?: {
116
+ type?: string;
117
+ payload?: Record<string, unknown> | null;
118
+ remoteMessage?: {
86
119
  data?: Record<string, unknown> | null;
87
- dataString?: string;
88
- };
89
- /** A remote push carries its raw payload: APNs `userInfo` on iOS, the FCM message on Android. */
90
- trigger?: {
91
- type?: string;
92
- payload?: Record<string, unknown> | null;
93
- remoteMessage?: {
94
- data?: Record<string, unknown> | null;
95
- } | null;
96
120
  } | null;
97
- };
121
+ } | null;
98
122
  };
99
123
  }
124
+ interface NotificationResponseLike {
125
+ actionIdentifier: string;
126
+ userText?: string;
127
+ notification: NotificationLike;
128
+ }
100
129
  interface NotificationActionLike {
101
130
  identifier: string;
102
131
  buttonTitle: string;
@@ -119,8 +148,10 @@ export interface PushNotificationsLike {
119
148
  getPermissionsAsync: () => Promise<PermissionResponseLike>;
120
149
  requestPermissionsAsync: () => Promise<PermissionResponseLike>;
121
150
  getDevicePushTokenAsync: () => Promise<DevicePushTokenLike>;
151
+ unregisterForNotificationsAsync: () => Promise<void>;
122
152
  addPushTokenListener: (listener: (token: DevicePushTokenLike) => void) => SubscriptionLike;
123
153
  addNotificationResponseReceivedListener: (listener: (response: NotificationResponseLike) => void) => SubscriptionLike;
154
+ addNotificationReceivedListener: (listener: (notification: NotificationLike) => void) => SubscriptionLike;
124
155
  getLastNotificationResponseAsync: () => Promise<NotificationResponseLike | null>;
125
156
  clearLastNotificationResponseAsync?: () => Promise<void>;
126
157
  setNotificationHandler: (handler: {
@@ -157,6 +188,8 @@ export interface PushModules {
157
188
  notifications: PushNotificationsLike;
158
189
  /** The bundle id, from expo-application. */
159
190
  applicationId: string | null;
191
+ /** expo-application's `getIosPushNotificationServiceEnvironmentAsync`; iOS only. */
192
+ getApnsEnvironment: () => Promise<'development' | 'production' | null>;
160
193
  }
161
194
  /**
162
195
  * Replace the native modules. For tests and the showcase, to reach paths a
@@ -170,6 +203,7 @@ export declare const setPushImplementation: (impl: PushModules | null) => void;
170
203
  * import throws on Android. Exported for `./task`; not on the `/push` entry.
171
204
  */
172
205
  export declare const resolveModules: () => PushModules | null;
206
+ export declare const toPushReceived: (notification: NotificationLike) => PushReceived;
173
207
  export declare const toPushAction: (response: NotificationResponseLike) => PushAction;
174
208
  export declare const toNotificationActions: (actions: PushCategoryAction[]) => NotificationActionLike[];
175
209
  /** The payload of the background notification task: routes button taps, ignores received pushes. */
@@ -183,13 +217,27 @@ export declare const initPush: (next: PushConfig) => void;
183
217
  /**
184
218
  * Asks for permission, reads the native device token and registers it through
185
219
  * `config.register`, then re-registers whenever the OS rotates the token.
220
+ * With `{ prompt: false }` it never shows the permission prompt: a phone
221
+ * already granted registers, and any other resolves with its status
222
+ * (`unknown` when the person has not been asked yet, `denied` when refused).
186
223
  */
187
- export declare const enablePush: () => Promise<"granted" | "denied" | "unavailable">;
224
+ export declare const enablePush: ({ prompt }?: EnablePushOptions) => Promise<PushStatus>;
188
225
  /**
189
- * Unregisters the device through `config.unregister` and stops following token
190
- * changes. Taps on notifications already delivered still reach `onOpen`.
226
+ * Unregisters the device through `config.unregister`, then stops following
227
+ * token changes. When the server call fails it rejects and push stays on, the
228
+ * token listener included, so the app can keep the person's choice and retry.
229
+ * Taps on notifications already delivered still reach `onOpen`.
191
230
  */
192
231
  export declare const disablePush: () => Promise<void>;
232
+ /**
233
+ * Turns push off on this phone alone, with no server call: stops following
234
+ * token changes and drops the native device token, so the server's next send to
235
+ * it fails and the server can delete it. For a sign-out where `disablePush`
236
+ * cannot reach the server, or has no session left to authorise the call. Clears
237
+ * the local state at once; the returned promise settles when the token is gone.
238
+ * `enablePush` waits for the drop before it reads a new token.
239
+ */
240
+ export declare const resetPush: () => Promise<void>;
193
241
  /** Sets the app icon badge; a no-op where push is unavailable or the launcher has no badge. */
194
242
  export declare const setBadgeCount: (count: number) => Promise<void>;
195
243
  /**
@@ -204,6 +252,13 @@ export declare const setCategories: (categories: PushCategory[]) => Promise<void
204
252
  * `/push/task`, which sets this handler too. Returns a function that removes it.
205
253
  */
206
254
  export declare const onAction: (handler: PushActionHandler) => (() => void);
255
+ /**
256
+ * Calls `handler` for each notification that arrives while the app is open,
257
+ * whether or not it is shown, so the app can refresh what it displays. Not
258
+ * called for a notification that arrives in the background; its tap still
259
+ * goes to `onOpen`. Returns a function that removes the handler.
260
+ */
261
+ export declare const onReceive: (handler: PushReceiveHandler) => (() => void);
207
262
  /** Shows a notification from the phone itself, for example when a button's request failed. */
208
263
  export declare const notifyLocal: ({ title, body, data }: PushLocalNotification) => Promise<void>;
209
264
  /** Push status for a settings switch, re-rendering when it changes. */
@@ -87,6 +87,11 @@ export interface Db {
87
87
  execute: (sql: string, params?: readonly unknown[]) => Promise<SqliteResultSet>;
88
88
  select: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Promise<T[]>;
89
89
  selectOne: <T = Record<string, unknown>>(sql: string, params?: readonly unknown[]) => Promise<T | null>;
90
+ /**
91
+ * Runs `work` alone on the connection: every other call on this database
92
+ * waits until it commits or rolls back. Inside `work` use only `tx`; a call
93
+ * on the database itself would wait for this transaction and never finish.
94
+ */
90
95
  transaction: <T>(work: (tx: Pick<Db, 'execute' | 'select' | 'selectOne'>) => Promise<T>) => Promise<T>;
91
96
  insert: (table: string, row: Record<string, unknown>) => Promise<number | undefined>;
92
97
  insertMany: (table: string, rows: Record<string, unknown>[]) => Promise<number>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/mobile-core",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
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.",
5
5
  "keywords": [
6
6
  "react-native",