@webority/mobile-core 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +4 -0
  2. package/lib/commonjs/errors/extractApiError.js +1 -1
  3. package/lib/commonjs/errors/index.js +1 -1
  4. package/lib/commonjs/formatters/currency.js +1 -1
  5. package/lib/commonjs/formatters/house.js +1 -1
  6. package/lib/commonjs/index.js +1 -1
  7. package/lib/commonjs/initSDK.js +1 -1
  8. package/lib/commonjs/logger/index.js +1 -1
  9. package/lib/commonjs/logger/redact.js +1 -1
  10. package/lib/commonjs/push/push.js +1 -1
  11. package/lib/commonjs/utils/imageCompression.js +1 -1
  12. package/lib/module/errors/extractApiError.js +1 -1
  13. package/lib/module/errors/index.js +1 -1
  14. package/lib/module/formatters/currency.js +1 -1
  15. package/lib/module/formatters/house.js +1 -1
  16. package/lib/module/index.js +1 -1
  17. package/lib/module/initSDK.js +1 -1
  18. package/lib/module/logger/index.js +1 -1
  19. package/lib/module/logger/redact.js +1 -1
  20. package/lib/module/push/push.js +1 -1
  21. package/lib/module/utils/imageCompression.js +1 -1
  22. package/lib/typescript/commonjs/errors/extractApiError.d.ts +9 -6
  23. package/lib/typescript/commonjs/errors/index.d.ts +1 -1
  24. package/lib/typescript/commonjs/formatters/currency.d.ts +10 -9
  25. package/lib/typescript/commonjs/formatters/house.d.ts +2 -36
  26. package/lib/typescript/commonjs/index.d.ts +3 -3
  27. package/lib/typescript/commonjs/initSDK.d.ts +9 -1
  28. package/lib/typescript/commonjs/logger/crashlytics.d.ts +3 -3
  29. package/lib/typescript/commonjs/logger/index.d.ts +20 -8
  30. package/lib/typescript/commonjs/logger/redact.d.ts +2 -0
  31. package/lib/typescript/commonjs/push/push.d.ts +2 -0
  32. package/lib/typescript/module/errors/extractApiError.d.ts +9 -6
  33. package/lib/typescript/module/errors/index.d.ts +1 -1
  34. package/lib/typescript/module/formatters/currency.d.ts +10 -9
  35. package/lib/typescript/module/formatters/house.d.ts +2 -36
  36. package/lib/typescript/module/index.d.ts +3 -3
  37. package/lib/typescript/module/initSDK.d.ts +9 -1
  38. package/lib/typescript/module/logger/crashlytics.d.ts +3 -3
  39. package/lib/typescript/module/logger/index.d.ts +20 -8
  40. package/lib/typescript/module/logger/redact.d.ts +2 -0
  41. package/lib/typescript/module/push/push.d.ts +2 -0
  42. package/package.json +1 -1
@@ -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,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?e.canAskAgain?"denied":"blocked":"undetermined",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 dismissAllNotifications=async()=>{const e=resolveModules();if(e)try{await e.notifications.dismissAllNotificationsAsync()}catch(e){Logger.warn("[push] dismissing the notifications failed",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
+ "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?e.canAskAgain?"denied":"blocked":"undetermined",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}},deviceTimeZone=()=>Intl.DateTimeFormat().resolvedOptions().timeZone||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,timeZone:deviceTimeZone()}};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 dismissAllNotifications=async()=>{const e=resolveModules();if(e)try{await e.notifications.dismissAllNotificationsAsync()}catch(e){Logger.warn("[push] dismissing the notifications failed",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{File}from"expo-file-system";import{ImageManipulator}from"expo-image-manipulator";import{Logger}from"../logger/index.js";export class ImageUnreadableError extends Error{constructor(e,t){super(`Image is unreadable (${t}): ${e||"<empty uri>"}`),this.name="ImageUnreadableError",this.uri=e}}const release=e=>{try{e?.release?.()}catch{}},expoBackend={stat:async e=>{const t=new File(e);return t.exists?null==t.size?null:{exists:!0,size:t.size}:{exists:!1,size:0}},remove:async e=>{new File(e).delete()},open:async e=>{const t=ImageManipulator.manipulate(e);let i;try{i=await t.renderAsync()}finally{release(t)}return{width:i.width,height:i.height,encode:async({width:e,height:t,quality:a,format:r})=>{const n=ImageManipulator.manipulate(i);let o=null;try{void 0===e&&void 0===t||n.resize({width:e??null,height:t??null}),o=await n.renderAsync();const i=await o.saveAsync({compress:a,format:r});return{uri:i.uri,width:i.width,height:i.height}}finally{release(o),release(n)}},close:()=>release(i)}}};let backend=null;export const setImageCompressorImplementation=e=>{backend=e};const resolveBackend=()=>backend??expoBackend,EIGHT_MB=8388608,THREE_MB=3145728;export const qualityForFileSize=e=>null==e?.7:e>8388608?.6:e>3145728?.7:.8;const DEFAULT_MAX_SIZE=1500,DEFAULT_MIN_QUALITY=.4,QUALITY_STEP=.1,SIZE_STEP=.75,MIN_SIDE=320,FORMAT={jpg:{encode:"jpeg",mimeType:"image/jpeg"},png:{encode:"png",mimeType:"image/png"},webp:{encode:"webp",mimeType:"image/webp"}},MIME_BY_EXTENSION={jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",heic:"image/heic",heif:"image/heif",bmp:"image/bmp",tif:"image/tiff",tiff:"image/tiff"},lastSegment=e=>{const t=e.split(/[?#]/)[0]??"";return decodeURIComponentSafe(t.slice(t.lastIndexOf("/")+1))},decodeURIComponentSafe=e=>{try{return decodeURIComponent(e)}catch{return e}},extensionOf=e=>{const t=e.lastIndexOf(".");return t>0?e.slice(t+1).toLowerCase():""},withoutExtension=e=>{const t=e.lastIndexOf(".");return t>0?e.slice(0,t):e},baseNameFor=(e,t)=>withoutExtension(t?.trim()||lastSegment(e))||"image",round1=e=>Math.round(10*e)/10,safeStat=async(e,t)=>{try{return await e.stat(t)}catch{return null}},safeRemove=async(e,t)=>{try{await e.remove(t)}catch{}},renderWithin=async(e,t,i,a,r)=>{const n=Math.max(t.width,t.height)>i?t.width>=t.height?{width:i}:{height:i}:{},o=await t.encode({...n,quality:a,format:r}),s=await safeStat(e,o.uri);if(s&&(!s.exists||0===s.size))throw await safeRemove(e,o.uri),new ImageUnreadableError(o.uri,"the encoder wrote an empty file");return{...o,size:s?.size??null}},originalResult=(e,t,i,a)=>{const r=extensionOf(lastSegment(e))||"jpg",n=MIME_BY_EXTENSION[r]??"image/jpeg",o=`${i}.${r}`;return{uri:e,width:a?.width??null,height:a?.height??null,size:t,mimeType:n,extension:r,fileName:o,compressed:!1,quality:null,part:{uri:e,name:o,type:n}}};export const compressImage=async(e,t={})=>{if(!e)throw new ImageUnreadableError(e,"no uri");const i=resolveBackend(),{maxSize:a=DEFAULT_MAX_SIZE,output:r="jpg",maxBytes:n,minQuality:o=DEFAULT_MIN_QUALITY,name:s}=t,l=FORMAT[r]??FORMAT.jpg,m=baseNameFor(e,s),g=await safeStat(i,e);if(g&&!g.exists)throw new ImageUnreadableError(e,"the file does not exist");if(g&&0===g.size)throw new ImageUnreadableError(e,"the file is empty");const h=g?.size??t.fileSize??null;let c;try{c=await i.open(e)}catch(t){throw Logger.error("[imageCompression] could not decode the image",t),new ImageUnreadableError(e,"the photo could not be decoded")}try{let e=t.quality??qualityForFileSize(h),s=a,g=await renderWithin(i,c,s,e,l.encode);if(void 0!==n)for(;null!==g.size&&g.size>n;){if("png"!==r&&round1(e-.1)>=o)e=round1(e-.1);else{const e=Math.max(g.width,g.height);if(e<=320)break;s=Math.max(320,Math.floor(.75*e))}const t=await renderWithin(i,c,s,e,l.encode);await safeRemove(i,g.uri),g=t}const p=`${m}.${r}`;return{uri:g.uri,width:g.width,height:g.height,size:g.size,mimeType:l.mimeType,extension:r,fileName:p,compressed:!0,quality:"png"===r?null:e,part:{uri:g.uri,name:p,type:l.mimeType}}}catch(t){if(t instanceof ImageUnreadableError)throw t;Logger.error("[imageCompression] encoding failed; retrying once as jpeg at quality 0.7",t);try{const e=await renderWithin(i,c,a,.7,"jpeg"),t=`${m}.jpg`;return{uri:e.uri,width:e.width,height:e.height,size:e.size,mimeType:"image/jpeg",extension:"jpg",fileName:t,compressed:!0,quality:.7,part:{uri:e.uri,name:t,type:"image/jpeg"}}}catch(t){if(t instanceof ImageUnreadableError)throw t;Logger.error("[imageCompression] jpeg retry failed",t);if(Math.max(c.width,c.height)<=a&&(void 0===n||null!==h&&h<=n))return originalResult(e,h,m,{width:c.width,height:c.height});throw new ImageUnreadableError(e,"the photo could not be prepared for upload")}}finally{c.close()}};
1
+ "use strict";import{File}from"expo-file-system";import{ImageManipulator}from"expo-image-manipulator";import{Logger}from"../logger/index.js";export class ImageUnreadableError extends Error{constructor(e,t){super(`Image is unreadable (${t}): ${e||"<empty uri>"}`),this.name="ImageUnreadableError",this.uri=e}}const release=e=>{try{e?.release?.()}catch{}},expoBackend={stat:async e=>{const t=new File(e);return t.exists?null==t.size?null:{exists:!0,size:t.size}:{exists:!1,size:0}},remove:async e=>{new File(e).delete()},open:async e=>{const t=ImageManipulator.manipulate(e);let i;try{i=await t.renderAsync()}finally{release(t)}return{width:i.width,height:i.height,encode:async({width:e,height:t,quality:a,format:r})=>{const n=ImageManipulator.manipulate(i);let o=null;try{void 0===e&&void 0===t||n.resize({width:e??null,height:t??null}),o=await n.renderAsync();const i=await o.saveAsync({compress:a,format:r});return{uri:i.uri,width:i.width,height:i.height}}finally{release(o),release(n)}},close:()=>release(i)}}};let backend=null;export const setImageCompressorImplementation=e=>{backend=e};const resolveBackend=()=>backend??expoBackend,EIGHT_MB=8388608,THREE_MB=3145728;export const qualityForFileSize=e=>null==e?.7:e>8388608?.6:e>3145728?.7:.8;const DEFAULT_MAX_SIZE=1500,DEFAULT_MIN_QUALITY=.4,QUALITY_STEP=.1,SIZE_STEP=.75,MIN_SIDE=320,FORMAT={jpg:{encode:"jpeg",mimeType:"image/jpeg"},png:{encode:"png",mimeType:"image/png"},webp:{encode:"webp",mimeType:"image/webp"}},MIME_BY_EXTENSION={jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",heic:"image/heic",heif:"image/heif",bmp:"image/bmp",tif:"image/tiff",tiff:"image/tiff"},lastSegment=e=>{const t=e.split(/[?#]/)[0]??"";return decodeURIComponentSafe(t.slice(t.lastIndexOf("/")+1))},decodeURIComponentSafe=e=>{try{return decodeURIComponent(e)}catch{return e}},extensionOf=e=>{const t=e.lastIndexOf(".");return t>0?e.slice(t+1).toLowerCase():""},withoutExtension=e=>{const t=e.lastIndexOf(".");return t>0?e.slice(0,t):e},baseNameFor=(e,t)=>withoutExtension(t?.trim()||lastSegment(e))||"image",round1=e=>Math.round(10*e)/10,safeStat=async(e,t)=>{try{return await e.stat(t)}catch{return null}},safeRemove=async(e,t)=>{try{await e.remove(t)}catch{}},renderWithin=async(e,t,i,a,r)=>{const n=Math.max(t.width,t.height)>i?t.width>=t.height?{width:i}:{height:i}:{},o=await t.encode({...n,quality:a,format:r}),s=await safeStat(e,o.uri);if(s&&(!s.exists||0===s.size))throw await safeRemove(e,o.uri),new ImageUnreadableError(o.uri,"the encoder wrote an empty file");return{...o,size:s?.size??null}},originalResult=(e,t,i,a)=>{const r=extensionOf(lastSegment(e))||"jpg",n=MIME_BY_EXTENSION[r]??"image/jpeg",o=`${i}.${r}`;return{uri:e,width:a?.width??null,height:a?.height??null,size:t,mimeType:n,extension:r,fileName:o,compressed:!1,quality:null,part:{uri:e,name:o,type:n}}};export const compressImage=async(e,t={})=>{if(!e)throw new ImageUnreadableError(e,"no uri");const i=resolveBackend(),{maxSize:a=DEFAULT_MAX_SIZE,output:r="jpg",maxBytes:n,minQuality:o=DEFAULT_MIN_QUALITY,name:s}=t,l=FORMAT[r]??FORMAT.jpg,m=baseNameFor(e,s),g=await safeStat(i,e);if(g&&!g.exists)throw new ImageUnreadableError(e,"the file does not exist");if(g&&0===g.size)throw new ImageUnreadableError(e,"the file is empty");const h=g?.size??t.fileSize??null;let c,p;try{c=await i.open(e)}catch(t){throw Logger.error("[imageCompression] could not decode the image",t),new ImageUnreadableError(e,"the photo could not be decoded")}try{let e=t.quality??qualityForFileSize(h),s=a;if(p=await renderWithin(i,c,s,e,l.encode),void 0!==n)for(;null!==p.size&&p.size>n;){if("png"!==r&&round1(e-.1)>=o)e=round1(e-.1);else{const e=Math.max(p.width,p.height);if(e<=320)break;s=Math.max(320,Math.floor(.75*e))}const t=await renderWithin(i,c,s,e,l.encode);await safeRemove(i,p.uri),p=t}const g=`${m}.${r}`;return{uri:p.uri,width:p.width,height:p.height,size:p.size,mimeType:l.mimeType,extension:r,fileName:g,compressed:!0,quality:"png"===r?null:e,part:{uri:p.uri,name:g,type:l.mimeType}}}catch(t){if(p&&await safeRemove(i,p.uri),t instanceof ImageUnreadableError)throw t;Logger.error("[imageCompression] encoding failed; retrying once as jpeg at quality 0.7",t);try{const e=await renderWithin(i,c,a,.7,"jpeg"),t=`${m}.jpg`;return{uri:e.uri,width:e.width,height:e.height,size:e.size,mimeType:"image/jpeg",extension:"jpg",fileName:t,compressed:!0,quality:.7,part:{uri:e.uri,name:t,type:"image/jpeg"}}}catch(t){if(t instanceof ImageUnreadableError)throw t;Logger.error("[imageCompression] jpeg retry failed",t);if(Math.max(c.width,c.height)<=a&&(void 0===n||null!==h&&h<=n))return originalResult(e,h,m,{width:c.width,height:c.height});throw new ImageUnreadableError(e,"the photo could not be prepared for upload")}}finally{c.close()}};
@@ -12,9 +12,9 @@
12
12
  * legacy `error`, `title`, then `Request failed (<status>)`. `title` is last because it is
13
13
  * the ProblemDetails category ("Bad Request"), not the curated sentence.
14
14
  *
15
- * Two things never become `message`: a 5xx body (our failure, nothing the user can act on)
16
- * and anything that reads like an exception dump. The server's text goes on `detail` for
17
- * the logger instead.
15
+ * Never a `message`: anything that reads like an exception dump, and a 5xx body unless it is
16
+ * coded (not `internal_error`) and its `message` is user-safe. The server's text goes on
17
+ * `detail` for the logger either way.
18
18
  */
19
19
  export interface ApiError {
20
20
  /** Stable snake_case branch key from the server (`not_found`, `validation_failed`). Branch on it, never render it. */
@@ -36,14 +36,17 @@ export declare const SERVER_FAILURE_MESSAGE = "Something went wrong on our side.
36
36
  export declare const OFFLINE_MESSAGE = "You appear to be offline. Check your connection and try again.";
37
37
  export declare const TIMEOUT_MESSAGE = "That took too long to respond. Check your connection and try again.";
38
38
  export declare const UNEXPECTED_MESSAGE = "Something went wrong. Please try again.";
39
+ export declare const UNREACHABLE_MESSAGE = "We couldn't reach the server. Check your connection and try again.";
39
40
  /** `code` for a request the caller cancelled (axios `CanceledError`). Never toasted. */
40
41
  export declare const REQUEST_CANCELED_CODE = "request_canceled";
41
42
  /** `code` for a response that failed schema validation at the service boundary. */
42
43
  export declare const CONTRACT_ERROR_CODE = "contract_error";
43
44
  /** `code` for a request that got no answer in time (axios `ECONNABORTED` / `ETIMEDOUT`). `offline` is true. */
44
45
  export declare const TIMEOUT_CODE = "timeout";
45
- /** `code` for a request that never reached the server (offline, DNS, CORS, a dropped connection). `offline` is true. */
46
+ /** `code` for a request that got no answer from the server (DNS, CORS, a dropped connection, a server that is down). `offline` is true. */
46
47
  export declare const NETWORK_CODE = "network";
48
+ /** `code` for a request refused because the device reports no connection. `offline` is true. */
49
+ export declare const OFFLINE_CODE = "offline";
47
50
  /** `Items[0].UnitPrice` -> `items[0].unitPrice`; `$.email` (System.Text.Json path) -> `email`. */
48
51
  export declare const toFieldPath: (key: string) => string;
49
52
  export declare const isApiError: (value: unknown) => value is ApiError;
@@ -57,8 +60,8 @@ export declare const isNoResponse: (error: unknown) => boolean;
57
60
  * The sentence a screen shows for a failure: the server's own curated message when it sent one,
58
61
  * otherwise the screen's `fallback` ("We couldn't save the project."), otherwise the library's
59
62
  * generic sentence. A lost answer always reads as the connection message, because "check your
60
- * connection" is more accurate than any screen's fallback. A 5xx, a cancelled request and a body
61
- * with no usable sentence count as "the server said nothing usable".
63
+ * connection" is more accurate than any screen's fallback. A 5xx without a curated sentence, a
64
+ * cancelled request and a body with no usable sentence count as "the server said nothing usable".
62
65
  *
63
66
  * `fallback: null` is the "say nothing" mode, for a screen that shows only what the server
64
67
  * curated (a live panel that keeps its last result): it returns null when the server said
@@ -1,3 +1,3 @@
1
1
  export type { ApiError } from './extractApiError';
2
- export { apiErrorMessage, CONTRACT_ERROR_CODE, extractApiError, isApiError, isNoResponse, NETWORK_CODE, OFFLINE_MESSAGE, REQUEST_CANCELED_CODE, SERVER_FAILURE_MESSAGE, TIMEOUT_CODE, TIMEOUT_MESSAGE, toFieldPath, UNEXPECTED_MESSAGE } from './extractApiError';
2
+ export { apiErrorMessage, CONTRACT_ERROR_CODE, extractApiError, isApiError, isNoResponse, NETWORK_CODE, OFFLINE_CODE, OFFLINE_MESSAGE, REQUEST_CANCELED_CODE, SERVER_FAILURE_MESSAGE, TIMEOUT_CODE, TIMEOUT_MESSAGE, toFieldPath, UNEXPECTED_MESSAGE, UNREACHABLE_MESSAGE } from './extractApiError';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -3,7 +3,7 @@
3
3
  * `signDisplay`, ...), passed through to Intl.NumberFormat.
4
4
  */
5
5
  export interface FormatCurrencyOptions extends Omit<Intl.NumberFormatOptions, 'style' | 'currency'> {
6
- /** ISO 4217 currency code. Defaults to `'INR'`. */
6
+ /** ISO 4217 currency code. A missing or empty one warns and prints INR. */
7
7
  currency?: string;
8
8
  /** BCP-47 locale tag. Defaults to the configured locale, else `'en-IN'` (Indian numbering system). */
9
9
  locale?: string;
@@ -20,9 +20,10 @@ export interface FormatCurrencyOptions extends Omit<Intl.NumberFormatOptions, 's
20
20
  empty?: string;
21
21
  }
22
22
  /**
23
- * Format an amount as currency. Defaults to **INR** with Indian numbering (`en-IN`),
24
- * which matches the standard convention across Webority projects. Called with no
25
- * options: "₹1,50,000" for 150000, "₹1,234.50" for 1234.5, and `''` for a missing amount.
23
+ * Format an amount as currency. Pass an ISO 4217 code: a missing or empty one warns and
24
+ * prints **INR** with Indian numbering (`en-IN`), the convention across Webority projects.
25
+ * With `{ currency: 'INR' }`: "₹1,50,000" for 150000, "₹1,234.50" for 1234.5, and `''` for a
26
+ * missing amount.
26
27
  * Once the app configures a locale (`configureFormatters({ locale })`), the amount is
27
28
  * written in that locale and a missing amount prints the configured placeholder ("-").
28
29
  * A configured `numberStyle` sets the decimal mark and group separator; the locale keeps
@@ -31,11 +32,11 @@ export interface FormatCurrencyOptions extends Omit<Intl.NumberFormatOptions, 's
31
32
  * use `formatMinor`.
32
33
  *
33
34
  * @example
34
- * formatCurrency(150000) // ₹1,50,000
35
- * formatCurrency(1234.5) // ₹1,234.50
36
- * formatCurrency(99, { currency: 'USD', locale: 'en-US' }) // $99
37
- * formatCurrency(150000, { stripSymbol: true }) // 1,50,000
38
- * formatCurrency(1500000, { notation: 'compact' }) // ₹15L
35
+ * formatCurrency(150000, { currency: 'INR' }) // ₹1,50,000
36
+ * formatCurrency(1234.5, { currency: 'INR' }) // ₹1,234.50
37
+ * formatCurrency(99, { currency: 'USD', locale: 'en-US' }) // $99
38
+ * formatCurrency(150000, { currency: 'INR', stripSymbol: true }) // 1,50,000
39
+ * formatCurrency(1500000, { currency: 'INR', notation: 'compact' }) // ₹15L
39
40
  */
40
41
  export declare const formatCurrency: (amount: number | string | null | undefined, options?: FormatCurrencyOptions) => string;
41
42
  //# sourceMappingURL=currency.d.ts.map
@@ -1,37 +1,3 @@
1
- /**
2
- * The house formatters: DD-MMM-YYYY dates, IST by default or the viewer's own
3
- * zone with a label, Indian digit grouping by default, money in minor units,
4
- * and one empty placeholder. Byte-identical in @webority/mobile-core and
5
- * @webority/web-core.
6
- *
7
- * Same output on Hermes and in every browser is the design constraint, so the
8
- * text is assembled here rather than by Intl. Engines disagree on month
9
- * abbreviations ("Sep" or "Sept"), on the space before AM/PM (a plain or a
10
- * narrow no-break space), on zone names ("IST" or "GMT+5:30") and on which
11
- * Intl options exist at all (Hermes has no RelativeTimeFormat and no
12
- * trailingZeroDisplay). IST and the viewer's zone are therefore computed with
13
- * plain Date arithmetic; Intl is used only to read the wall clock of an
14
- * explicitly named IANA zone, through formatToParts with numeric fields, to
15
- * read the locale's 12- or 24-hour clock, and where a caller asks for Intl
16
- * output itself (Intl number options, `formatRelative({ intl: true })`).
17
- *
18
- * Called with no options (house defaults: zone IST, locale en-IN, empty "-"):
19
- * - formatDate(v) "08-Apr-2026"; formatTime(v) "8:05 PM"; formatDateTime(v) "08-Apr-2026, 8:05 PM";
20
- * - formatRelative(v) "5 minutes ago", "in 3 days", "just now";
21
- * - formatNumber(v) "1,23,456.789" (Indian grouping, up to 3 decimals);
22
- * - formatPercent(0.42) "42%"; formatMinor(15000000) "₹1,50,000.00";
23
- * - a missing or unreadable value "-".
24
- *
25
- * With a configured locale (`configureFormatters({ locale })`), the words and separators come
26
- * from Intl for that locale and the date order stays DD-MMM-YYYY. For en-IN in the viewer zone:
27
- * "08-Sept-2026", "08:05 pm", "08-Sept-2026, 08:05 pm IST", "yesterday", "12,34,567.891",
28
- * "₹1,50,000". For de-DE: "08-Sept-2026", "20:05", "1.234.567,891".
29
- *
30
- * A configured `numberStyle` puts its decimal mark and group separator in every number and amount,
31
- * whichever path wrote it: "1.23.456,789" for `'CommaWithPointGroups'` with the house text. A
32
- * configured `dateOrder` other than DD-MMM-YYYY writes the date as digits from the wall clock,
33
- * in every locale: "08/04/2026", "04/08/2026", "2026-04-08".
34
- */
35
1
  import { type NumberGrouping, type ZoneMode } from './formatConfig';
36
2
  /** A date the house formatters accept; null, undefined and "" read as missing. */
37
3
  export type DateValue = Date | string | number | null | undefined;
@@ -96,7 +62,7 @@ export interface FormatPercentOptions extends FormatNumberOptions {
96
62
  scale?: 'ratio' | 'percent';
97
63
  }
98
64
  export interface FormatMinorOptions {
99
- /** ISO 4217 code. Defaults to `'INR'`. */
65
+ /** ISO 4217 code. A missing or empty one warns and prints INR. */
100
66
  currency?: string;
101
67
  /** Defaults to Indian grouping for INR and international grouping for every other currency. */
102
68
  grouping?: NumberGrouping;
@@ -138,7 +104,7 @@ export declare const formatNumber: (value: number | string | null | undefined, o
138
104
  export declare const formatPercent: (value: number | string | null | undefined, options?: FormatPercentOptions) => string;
139
105
  /**
140
106
  * Money held in minor units (paise, cents), which is how every API carries
141
- * it: formatMinor(15000000) gives "₹1,50,000.00". The split into whole and
107
+ * it: formatMinor(15000000, { currency: 'INR' }) gives "₹1,50,000.00". The split into whole and
142
108
  * fractional units is done on the integer's digits, so no floating-point
143
109
  * division can shift a paisa. A fractional input is rounded to the nearest
144
110
  * minor unit. A currency without a known symbol prints its code: "AED 1,000.00".
@@ -52,15 +52,15 @@ export { getConfig, getConfigValue, resetConfig, setConfig } from './config';
52
52
  export type { DeepLinkConfig, DeepLinkRoute, ParsedLink } from './deepLink';
53
53
  export { DeepLink, parseDeepLink, useDeepLink } from './deepLink';
54
54
  export type { ApiError } from './errors';
55
- export { apiErrorMessage, CONTRACT_ERROR_CODE, extractApiError, isApiError, isNoResponse, NETWORK_CODE, OFFLINE_MESSAGE, REQUEST_CANCELED_CODE, SERVER_FAILURE_MESSAGE, TIMEOUT_CODE, TIMEOUT_MESSAGE, toFieldPath, UNEXPECTED_MESSAGE } from './errors';
55
+ export { apiErrorMessage, CONTRACT_ERROR_CODE, extractApiError, isApiError, isNoResponse, NETWORK_CODE, OFFLINE_CODE, OFFLINE_MESSAGE, REQUEST_CANCELED_CODE, SERVER_FAILURE_MESSAGE, TIMEOUT_CODE, TIMEOUT_MESSAGE, toFieldPath, UNEXPECTED_MESSAGE, UNREACHABLE_MESSAGE } from './errors';
56
56
  export type { DateInput, DateOrder, DateStyle, DateValue, FormatCurrencyOptions, FormatDateOptions, FormatDateTimeOptions, FormatMinorOptions, FormatNumberOptions, FormatPercentOptions, FormatPhoneOptions, FormatRelativeOptions, FormatTimeOptions, FormatterConfig, NumberGrouping, NumberStyle, PhoneCountry, PhoneCountryEntry, PhoneNumber, PhoneNumberType, PhoneParts, PhoneValidationKind, TimeStyle, ZoneMode, ZoneOption } from './formatters';
57
57
  export { AsYouType, addDays, clampNationalNumber, configureFormatters, countryFlagEmoji, detectPhoneCountry, diffMs, formatCompactNumber, formatCurrency, formatDate, formatDateDDMMMYYYY, formatDateISO, formatDatePattern, formatDateTime, formatMinor, formatNumber, formatPercent, formatPhone, formatPhoneAsTyped, formatRelative, formatRelativeTime, formatTime, formatTime12h, formatTimeInWindowsZone, formatTimeInZone, getCountries, getCountryCallingCode, getFormatterConfig, getInitials, getPhoneCountries, isDateAfter, isDateBefore, isSameDay, isSupportedCountry, isValidNationalNumber, isValidPhoneNumber, maxNationalNumberLength, normalizePhone, parsePhoneNumber, parsePhoneNumberFromString, splitPhoneNumber, startOfDay, validatePhoneNumberLength } from './formatters';
58
58
  export type { InitConfig } from './initSDK';
59
59
  export { initWebority } from './initSDK';
60
60
  export type { TokenRefreshConfig, TokenRefreshResult, UserAuthOptions } from './initUserAuth';
61
61
  export { initUserAuth } from './initUserAuth';
62
- export type { DeviceContext, LoggerOptions, LoggerType, RemoteLogger, RequestFailedContext } from './logger';
63
- export { configureLogger, getDeviceContext, Logger, setDeviceContext, setRemoteLogger } from './logger';
62
+ export type { DeviceContext, LoggerBreadcrumb, LoggerOptions, LoggerType, RemoteLogger, RequestFailedContext } from './logger';
63
+ export { configureLogger, getDeviceContext, Logger, setDeviceContext } from './logger';
64
64
  export type { CrashlyticsModule } from './logger/crashlytics';
65
65
  export { crashlyticsRemoteLogger } from './logger/crashlytics';
66
66
  export type { NetworkConnectionType, NetworkStatus, NetworkStatusLike, NetworkStatusListener } from './network';
@@ -4,11 +4,17 @@
4
4
  */
5
5
  import { type KeychainLike } from './auth/authStore';
6
6
  import { type Environment, type LogLevel } from './config';
7
+ import { type RemoteLogger } from './logger';
7
8
  import { type NetInfoLike } from './network/networkStatus';
8
9
  import { setStorageImplementation } from './storage';
9
10
  export interface InitConfig {
10
11
  /** Your API base URL (required) */
11
12
  baseUrl: string;
13
+ /**
14
+ * Where errors and breadcrumbs go: a RemoteLogger adapter (e.g. `crashlyticsRemoteLogger`), or
15
+ * `null` for a build without crash reporting. Required: omitting it throws at start-up.
16
+ */
17
+ remoteLogger: RemoteLogger | null;
12
18
  /** Enable/disable logging. Defaults to `true`. */
13
19
  loggingEnabled?: boolean;
14
20
  /** Log level. Defaults to `'info'` in development and `'warn'` in a release build. */
@@ -68,13 +74,15 @@ export interface InitConfig {
68
74
  * @example
69
75
  * // Basic setup
70
76
  * initWebority({
71
- * baseUrl: 'https://api.myapp.com'
77
+ * baseUrl: 'https://api.myapp.com',
78
+ * remoteLogger: null
72
79
  * });
73
80
  *
74
81
  * @example
75
82
  * // Full configuration
76
83
  * initWebority({
77
84
  * baseUrl: 'https://api.myapp.com',
85
+ * remoteLogger: crashlyticsRemoteLogger(crashlytics),
78
86
  * loggingEnabled: __DEV__,
79
87
  * logLevel: 'warn',
80
88
  * environment: __DEV__ ? 'Development' : 'Production'
@@ -12,13 +12,13 @@ export interface CrashlyticsModule<Instance = unknown> {
12
12
  setAttribute: (crashlytics: Instance, name: string, value: string) => Promise<unknown>;
13
13
  }
14
14
  /**
15
- * Builds the `setRemoteLogger` adapter for Firebase Crashlytics:
15
+ * Builds the `remoteLogger` adapter for Firebase Crashlytics, passed to `initWebority`:
16
16
  *
17
17
  * ```ts
18
18
  * import * as crashlytics from '@react-native-firebase/crashlytics';
19
- * import { crashlyticsRemoteLogger, setRemoteLogger } from '@webority/mobile-core';
19
+ * import { crashlyticsRemoteLogger, initWebority } from '@webority/mobile-core';
20
20
  *
21
- * setRemoteLogger(crashlyticsRemoteLogger(crashlytics));
21
+ * initWebority({ baseUrl, remoteLogger: crashlyticsRemoteLogger(crashlytics) });
22
22
  * ```
23
23
  *
24
24
  * Breadcrumbs and log lines become Crashlytics log lines, errors become
@@ -1,8 +1,8 @@
1
1
  import { type LogLevel } from '../config';
2
2
  type LogFn = (message: string, ...optionalParams: unknown[]) => void;
3
3
  /**
4
- * Optional remote sink. Apps wire this via `setRemoteLogger()` to forward errors
5
- * and breadcrumbs to Crashlytics, Sentry, Bugsnag, etc. — without making the
4
+ * Remote sink. Apps pass one to `initWebority({ remoteLogger })` to forward errors
5
+ * and breadcrumbs to Crashlytics, Sentry, Bugsnag, etc., without making the
6
6
  * library depend on any of them.
7
7
  *
8
8
  * Every method is optional; the library calls only what the adapter implements.
@@ -20,10 +20,7 @@ export interface RemoteLogger {
20
20
  /** Forward a leveled console message. */
21
21
  log?: (level: LogLevel, message: string, optionalParams: unknown[]) => void;
22
22
  }
23
- /**
24
- * Inject a remote logger adapter (e.g. Firebase Crashlytics, Sentry).
25
- * Pass `null` to detach. Safe to call before or after `initWebority`.
26
- */
23
+ /** Set by `initWebority`; `null` detaches. Not part of the package's public API. */
27
24
  export declare const setRemoteLogger: (impl: RemoteLogger | null) => void;
28
25
  /**
29
26
  * Device / app context attached once at boot (platform, OS version, app version,
@@ -47,6 +44,12 @@ export interface DeviceContext {
47
44
  */
48
45
  export declare const setDeviceContext: (context: DeviceContext | null) => void;
49
46
  export declare const getDeviceContext: () => DeviceContext | null;
47
+ export interface LoggerBreadcrumb {
48
+ message: string;
49
+ data?: Record<string, unknown>;
50
+ /** When it was recorded, ISO 8601 in UTC. */
51
+ timestampUtc: string;
52
+ }
50
53
  /** Tuning knobs for screen history and duplicate-error suppression. */
51
54
  export interface LoggerOptions {
52
55
  /** Identical errors within this window are collapsed. Default 5000ms. 0 disables dedupe. */
@@ -55,8 +58,15 @@ export interface LoggerOptions {
55
58
  dedupeMaxPerWindow?: number;
56
59
  /** Cap on tracked screen history. Default 20. */
57
60
  screenHistoryLimit?: number;
61
+ /** Extra keys masked in logs, added to the built-in list. */
62
+ redactKeys?: string[];
63
+ /** Breadcrumbs kept for Logger.getBreadcrumbs(). Default 50. */
64
+ breadcrumbLimit?: number;
58
65
  }
59
- /** Configure dedupe window/threshold and screen history size. Merges with current options. */
66
+ /**
67
+ * Configure dedupe, screen history, breadcrumbs and redaction. Merges with current options;
68
+ * a `redactKeys` list replaces the app keys given before, and omitting it keeps them.
69
+ */
60
70
  export declare const configureLogger: (options: LoggerOptions) => void;
61
71
  /** What `Logger.requestFailed` is told about the call beside the error itself. */
62
72
  export interface RequestFailedContext {
@@ -76,7 +86,7 @@ export interface LoggerType {
76
86
  info: LogFn;
77
87
  warn: LogFn;
78
88
  error: LogFn;
79
- /** Append a breadcrumb to the remote sink (and log at info level). */
89
+ /** Append a breadcrumb to the ring and the remote sink (and log at info level). Data is redacted before it is kept. */
80
90
  breadcrumb: (message: string, data?: Record<string, unknown>) => void;
81
91
  /** Record a non-fatal error with optional context. */
82
92
  recordError: (error: unknown, context?: Record<string, unknown>) => void;
@@ -92,6 +102,8 @@ export interface LoggerType {
92
102
  getCurrentScreen: () => string | null;
93
103
  /** Bounded history of tracked screen names, oldest first. */
94
104
  getScreenHistory: () => string[];
105
+ /** Copy of the breadcrumb ring, oldest first, capped at configureLogger({ breadcrumbLimit }). */
106
+ getBreadcrumbs: () => LoggerBreadcrumb[];
95
107
  }
96
108
  export declare const Logger: LoggerType;
97
109
  export {};
@@ -1,3 +1,5 @@
1
+ /** Rebuilds the masking rules from the built-in list plus `extraKeys`. Replaces any earlier extras. */
2
+ export declare const configureRedaction: (extraKeys: readonly string[]) => void;
1
3
  export declare const redactString: (input: string) => string;
2
4
  /** Returns a copy with secrets masked; Errors are masked in place so identity and stack survive. */
3
5
  export declare const redact: <T>(value: T) => T;
@@ -14,6 +14,8 @@ export interface PushDevice {
14
14
  * (the simulator), so the server falls back to its own setting.
15
15
  */
16
16
  apnsEnvironment: PushApnsEnvironment | null;
17
+ /** The device's IANA time zone, such as `Europe/London`. Null when the platform cannot name it. */
18
+ timeZone: string | null;
17
19
  }
18
20
  /** A notification that arrived while the app was open. */
19
21
  export interface PushReceived {
@@ -12,9 +12,9 @@
12
12
  * legacy `error`, `title`, then `Request failed (<status>)`. `title` is last because it is
13
13
  * the ProblemDetails category ("Bad Request"), not the curated sentence.
14
14
  *
15
- * Two things never become `message`: a 5xx body (our failure, nothing the user can act on)
16
- * and anything that reads like an exception dump. The server's text goes on `detail` for
17
- * the logger instead.
15
+ * Never a `message`: anything that reads like an exception dump, and a 5xx body unless it is
16
+ * coded (not `internal_error`) and its `message` is user-safe. The server's text goes on
17
+ * `detail` for the logger either way.
18
18
  */
19
19
  export interface ApiError {
20
20
  /** Stable snake_case branch key from the server (`not_found`, `validation_failed`). Branch on it, never render it. */
@@ -36,14 +36,17 @@ export declare const SERVER_FAILURE_MESSAGE = "Something went wrong on our side.
36
36
  export declare const OFFLINE_MESSAGE = "You appear to be offline. Check your connection and try again.";
37
37
  export declare const TIMEOUT_MESSAGE = "That took too long to respond. Check your connection and try again.";
38
38
  export declare const UNEXPECTED_MESSAGE = "Something went wrong. Please try again.";
39
+ export declare const UNREACHABLE_MESSAGE = "We couldn't reach the server. Check your connection and try again.";
39
40
  /** `code` for a request the caller cancelled (axios `CanceledError`). Never toasted. */
40
41
  export declare const REQUEST_CANCELED_CODE = "request_canceled";
41
42
  /** `code` for a response that failed schema validation at the service boundary. */
42
43
  export declare const CONTRACT_ERROR_CODE = "contract_error";
43
44
  /** `code` for a request that got no answer in time (axios `ECONNABORTED` / `ETIMEDOUT`). `offline` is true. */
44
45
  export declare const TIMEOUT_CODE = "timeout";
45
- /** `code` for a request that never reached the server (offline, DNS, CORS, a dropped connection). `offline` is true. */
46
+ /** `code` for a request that got no answer from the server (DNS, CORS, a dropped connection, a server that is down). `offline` is true. */
46
47
  export declare const NETWORK_CODE = "network";
48
+ /** `code` for a request refused because the device reports no connection. `offline` is true. */
49
+ export declare const OFFLINE_CODE = "offline";
47
50
  /** `Items[0].UnitPrice` -> `items[0].unitPrice`; `$.email` (System.Text.Json path) -> `email`. */
48
51
  export declare const toFieldPath: (key: string) => string;
49
52
  export declare const isApiError: (value: unknown) => value is ApiError;
@@ -57,8 +60,8 @@ export declare const isNoResponse: (error: unknown) => boolean;
57
60
  * The sentence a screen shows for a failure: the server's own curated message when it sent one,
58
61
  * otherwise the screen's `fallback` ("We couldn't save the project."), otherwise the library's
59
62
  * generic sentence. A lost answer always reads as the connection message, because "check your
60
- * connection" is more accurate than any screen's fallback. A 5xx, a cancelled request and a body
61
- * with no usable sentence count as "the server said nothing usable".
63
+ * connection" is more accurate than any screen's fallback. A 5xx without a curated sentence, a
64
+ * cancelled request and a body with no usable sentence count as "the server said nothing usable".
62
65
  *
63
66
  * `fallback: null` is the "say nothing" mode, for a screen that shows only what the server
64
67
  * curated (a live panel that keeps its last result): it returns null when the server said
@@ -1,3 +1,3 @@
1
1
  export type { ApiError } from './extractApiError.js';
2
- export { apiErrorMessage, CONTRACT_ERROR_CODE, extractApiError, isApiError, isNoResponse, NETWORK_CODE, OFFLINE_MESSAGE, REQUEST_CANCELED_CODE, SERVER_FAILURE_MESSAGE, TIMEOUT_CODE, TIMEOUT_MESSAGE, toFieldPath, UNEXPECTED_MESSAGE } from './extractApiError.js';
2
+ export { apiErrorMessage, CONTRACT_ERROR_CODE, extractApiError, isApiError, isNoResponse, NETWORK_CODE, OFFLINE_CODE, OFFLINE_MESSAGE, REQUEST_CANCELED_CODE, SERVER_FAILURE_MESSAGE, TIMEOUT_CODE, TIMEOUT_MESSAGE, toFieldPath, UNEXPECTED_MESSAGE, UNREACHABLE_MESSAGE } from './extractApiError.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -3,7 +3,7 @@
3
3
  * `signDisplay`, ...), passed through to Intl.NumberFormat.
4
4
  */
5
5
  export interface FormatCurrencyOptions extends Omit<Intl.NumberFormatOptions, 'style' | 'currency'> {
6
- /** ISO 4217 currency code. Defaults to `'INR'`. */
6
+ /** ISO 4217 currency code. A missing or empty one warns and prints INR. */
7
7
  currency?: string;
8
8
  /** BCP-47 locale tag. Defaults to the configured locale, else `'en-IN'` (Indian numbering system). */
9
9
  locale?: string;
@@ -20,9 +20,10 @@ export interface FormatCurrencyOptions extends Omit<Intl.NumberFormatOptions, 's
20
20
  empty?: string;
21
21
  }
22
22
  /**
23
- * Format an amount as currency. Defaults to **INR** with Indian numbering (`en-IN`),
24
- * which matches the standard convention across Webority projects. Called with no
25
- * options: "₹1,50,000" for 150000, "₹1,234.50" for 1234.5, and `''` for a missing amount.
23
+ * Format an amount as currency. Pass an ISO 4217 code: a missing or empty one warns and
24
+ * prints **INR** with Indian numbering (`en-IN`), the convention across Webority projects.
25
+ * With `{ currency: 'INR' }`: "₹1,50,000" for 150000, "₹1,234.50" for 1234.5, and `''` for a
26
+ * missing amount.
26
27
  * Once the app configures a locale (`configureFormatters({ locale })`), the amount is
27
28
  * written in that locale and a missing amount prints the configured placeholder ("-").
28
29
  * A configured `numberStyle` sets the decimal mark and group separator; the locale keeps
@@ -31,11 +32,11 @@ export interface FormatCurrencyOptions extends Omit<Intl.NumberFormatOptions, 's
31
32
  * use `formatMinor`.
32
33
  *
33
34
  * @example
34
- * formatCurrency(150000) // ₹1,50,000
35
- * formatCurrency(1234.5) // ₹1,234.50
36
- * formatCurrency(99, { currency: 'USD', locale: 'en-US' }) // $99
37
- * formatCurrency(150000, { stripSymbol: true }) // 1,50,000
38
- * formatCurrency(1500000, { notation: 'compact' }) // ₹15L
35
+ * formatCurrency(150000, { currency: 'INR' }) // ₹1,50,000
36
+ * formatCurrency(1234.5, { currency: 'INR' }) // ₹1,234.50
37
+ * formatCurrency(99, { currency: 'USD', locale: 'en-US' }) // $99
38
+ * formatCurrency(150000, { currency: 'INR', stripSymbol: true }) // 1,50,000
39
+ * formatCurrency(1500000, { currency: 'INR', notation: 'compact' }) // ₹15L
39
40
  */
40
41
  export declare const formatCurrency: (amount: number | string | null | undefined, options?: FormatCurrencyOptions) => string;
41
42
  //# sourceMappingURL=currency.d.ts.map
@@ -1,37 +1,3 @@
1
- /**
2
- * The house formatters: DD-MMM-YYYY dates, IST by default or the viewer's own
3
- * zone with a label, Indian digit grouping by default, money in minor units,
4
- * and one empty placeholder. Byte-identical in @webority/mobile-core and
5
- * @webority/web-core.
6
- *
7
- * Same output on Hermes and in every browser is the design constraint, so the
8
- * text is assembled here rather than by Intl. Engines disagree on month
9
- * abbreviations ("Sep" or "Sept"), on the space before AM/PM (a plain or a
10
- * narrow no-break space), on zone names ("IST" or "GMT+5:30") and on which
11
- * Intl options exist at all (Hermes has no RelativeTimeFormat and no
12
- * trailingZeroDisplay). IST and the viewer's zone are therefore computed with
13
- * plain Date arithmetic; Intl is used only to read the wall clock of an
14
- * explicitly named IANA zone, through formatToParts with numeric fields, to
15
- * read the locale's 12- or 24-hour clock, and where a caller asks for Intl
16
- * output itself (Intl number options, `formatRelative({ intl: true })`).
17
- *
18
- * Called with no options (house defaults: zone IST, locale en-IN, empty "-"):
19
- * - formatDate(v) "08-Apr-2026"; formatTime(v) "8:05 PM"; formatDateTime(v) "08-Apr-2026, 8:05 PM";
20
- * - formatRelative(v) "5 minutes ago", "in 3 days", "just now";
21
- * - formatNumber(v) "1,23,456.789" (Indian grouping, up to 3 decimals);
22
- * - formatPercent(0.42) "42%"; formatMinor(15000000) "₹1,50,000.00";
23
- * - a missing or unreadable value "-".
24
- *
25
- * With a configured locale (`configureFormatters({ locale })`), the words and separators come
26
- * from Intl for that locale and the date order stays DD-MMM-YYYY. For en-IN in the viewer zone:
27
- * "08-Sept-2026", "08:05 pm", "08-Sept-2026, 08:05 pm IST", "yesterday", "12,34,567.891",
28
- * "₹1,50,000". For de-DE: "08-Sept-2026", "20:05", "1.234.567,891".
29
- *
30
- * A configured `numberStyle` puts its decimal mark and group separator in every number and amount,
31
- * whichever path wrote it: "1.23.456,789" for `'CommaWithPointGroups'` with the house text. A
32
- * configured `dateOrder` other than DD-MMM-YYYY writes the date as digits from the wall clock,
33
- * in every locale: "08/04/2026", "04/08/2026", "2026-04-08".
34
- */
35
1
  import { type NumberGrouping, type ZoneMode } from './formatConfig.js';
36
2
  /** A date the house formatters accept; null, undefined and "" read as missing. */
37
3
  export type DateValue = Date | string | number | null | undefined;
@@ -96,7 +62,7 @@ export interface FormatPercentOptions extends FormatNumberOptions {
96
62
  scale?: 'ratio' | 'percent';
97
63
  }
98
64
  export interface FormatMinorOptions {
99
- /** ISO 4217 code. Defaults to `'INR'`. */
65
+ /** ISO 4217 code. A missing or empty one warns and prints INR. */
100
66
  currency?: string;
101
67
  /** Defaults to Indian grouping for INR and international grouping for every other currency. */
102
68
  grouping?: NumberGrouping;
@@ -138,7 +104,7 @@ export declare const formatNumber: (value: number | string | null | undefined, o
138
104
  export declare const formatPercent: (value: number | string | null | undefined, options?: FormatPercentOptions) => string;
139
105
  /**
140
106
  * Money held in minor units (paise, cents), which is how every API carries
141
- * it: formatMinor(15000000) gives "₹1,50,000.00". The split into whole and
107
+ * it: formatMinor(15000000, { currency: 'INR' }) gives "₹1,50,000.00". The split into whole and
142
108
  * fractional units is done on the integer's digits, so no floating-point
143
109
  * division can shift a paisa. A fractional input is rounded to the nearest
144
110
  * minor unit. A currency without a known symbol prints its code: "AED 1,000.00".
@@ -52,15 +52,15 @@ export { getConfig, getConfigValue, resetConfig, setConfig } from './config/inde
52
52
  export type { DeepLinkConfig, DeepLinkRoute, ParsedLink } from './deepLink/index.js';
53
53
  export { DeepLink, parseDeepLink, useDeepLink } from './deepLink/index.js';
54
54
  export type { ApiError } from './errors/index.js';
55
- export { apiErrorMessage, CONTRACT_ERROR_CODE, extractApiError, isApiError, isNoResponse, NETWORK_CODE, OFFLINE_MESSAGE, REQUEST_CANCELED_CODE, SERVER_FAILURE_MESSAGE, TIMEOUT_CODE, TIMEOUT_MESSAGE, toFieldPath, UNEXPECTED_MESSAGE } from './errors/index.js';
55
+ export { apiErrorMessage, CONTRACT_ERROR_CODE, extractApiError, isApiError, isNoResponse, NETWORK_CODE, OFFLINE_CODE, OFFLINE_MESSAGE, REQUEST_CANCELED_CODE, SERVER_FAILURE_MESSAGE, TIMEOUT_CODE, TIMEOUT_MESSAGE, toFieldPath, UNEXPECTED_MESSAGE, UNREACHABLE_MESSAGE } from './errors/index.js';
56
56
  export type { DateInput, DateOrder, DateStyle, DateValue, FormatCurrencyOptions, FormatDateOptions, FormatDateTimeOptions, FormatMinorOptions, FormatNumberOptions, FormatPercentOptions, FormatPhoneOptions, FormatRelativeOptions, FormatTimeOptions, FormatterConfig, NumberGrouping, NumberStyle, PhoneCountry, PhoneCountryEntry, PhoneNumber, PhoneNumberType, PhoneParts, PhoneValidationKind, TimeStyle, ZoneMode, ZoneOption } from './formatters/index.js';
57
57
  export { AsYouType, addDays, clampNationalNumber, configureFormatters, countryFlagEmoji, detectPhoneCountry, diffMs, formatCompactNumber, formatCurrency, formatDate, formatDateDDMMMYYYY, formatDateISO, formatDatePattern, formatDateTime, formatMinor, formatNumber, formatPercent, formatPhone, formatPhoneAsTyped, formatRelative, formatRelativeTime, formatTime, formatTime12h, formatTimeInWindowsZone, formatTimeInZone, getCountries, getCountryCallingCode, getFormatterConfig, getInitials, getPhoneCountries, isDateAfter, isDateBefore, isSameDay, isSupportedCountry, isValidNationalNumber, isValidPhoneNumber, maxNationalNumberLength, normalizePhone, parsePhoneNumber, parsePhoneNumberFromString, splitPhoneNumber, startOfDay, validatePhoneNumberLength } from './formatters/index.js';
58
58
  export type { InitConfig } from './initSDK.js';
59
59
  export { initWebority } from './initSDK.js';
60
60
  export type { TokenRefreshConfig, TokenRefreshResult, UserAuthOptions } from './initUserAuth.js';
61
61
  export { initUserAuth } from './initUserAuth.js';
62
- export type { DeviceContext, LoggerOptions, LoggerType, RemoteLogger, RequestFailedContext } from './logger/index.js';
63
- export { configureLogger, getDeviceContext, Logger, setDeviceContext, setRemoteLogger } from './logger/index.js';
62
+ export type { DeviceContext, LoggerBreadcrumb, LoggerOptions, LoggerType, RemoteLogger, RequestFailedContext } from './logger/index.js';
63
+ export { configureLogger, getDeviceContext, Logger, setDeviceContext } from './logger/index.js';
64
64
  export type { CrashlyticsModule } from './logger/crashlytics.js';
65
65
  export { crashlyticsRemoteLogger } from './logger/crashlytics.js';
66
66
  export type { NetworkConnectionType, NetworkStatus, NetworkStatusLike, NetworkStatusListener } from './network/index.js';
@@ -4,11 +4,17 @@
4
4
  */
5
5
  import { type KeychainLike } from './auth/authStore.js';
6
6
  import { type Environment, type LogLevel } from './config/index.js';
7
+ import { type RemoteLogger } from './logger/index.js';
7
8
  import { type NetInfoLike } from './network/networkStatus.js';
8
9
  import { setStorageImplementation } from './storage/index.js';
9
10
  export interface InitConfig {
10
11
  /** Your API base URL (required) */
11
12
  baseUrl: string;
13
+ /**
14
+ * Where errors and breadcrumbs go: a RemoteLogger adapter (e.g. `crashlyticsRemoteLogger`), or
15
+ * `null` for a build without crash reporting. Required: omitting it throws at start-up.
16
+ */
17
+ remoteLogger: RemoteLogger | null;
12
18
  /** Enable/disable logging. Defaults to `true`. */
13
19
  loggingEnabled?: boolean;
14
20
  /** Log level. Defaults to `'info'` in development and `'warn'` in a release build. */
@@ -68,13 +74,15 @@ export interface InitConfig {
68
74
  * @example
69
75
  * // Basic setup
70
76
  * initWebority({
71
- * baseUrl: 'https://api.myapp.com'
77
+ * baseUrl: 'https://api.myapp.com',
78
+ * remoteLogger: null
72
79
  * });
73
80
  *
74
81
  * @example
75
82
  * // Full configuration
76
83
  * initWebority({
77
84
  * baseUrl: 'https://api.myapp.com',
85
+ * remoteLogger: crashlyticsRemoteLogger(crashlytics),
78
86
  * loggingEnabled: __DEV__,
79
87
  * logLevel: 'warn',
80
88
  * environment: __DEV__ ? 'Development' : 'Production'
@@ -12,13 +12,13 @@ export interface CrashlyticsModule<Instance = unknown> {
12
12
  setAttribute: (crashlytics: Instance, name: string, value: string) => Promise<unknown>;
13
13
  }
14
14
  /**
15
- * Builds the `setRemoteLogger` adapter for Firebase Crashlytics:
15
+ * Builds the `remoteLogger` adapter for Firebase Crashlytics, passed to `initWebority`:
16
16
  *
17
17
  * ```ts
18
18
  * import * as crashlytics from '@react-native-firebase/crashlytics';
19
- * import { crashlyticsRemoteLogger, setRemoteLogger } from '@webority/mobile-core';
19
+ * import { crashlyticsRemoteLogger, initWebority } from '@webority/mobile-core';
20
20
  *
21
- * setRemoteLogger(crashlyticsRemoteLogger(crashlytics));
21
+ * initWebority({ baseUrl, remoteLogger: crashlyticsRemoteLogger(crashlytics) });
22
22
  * ```
23
23
  *
24
24
  * Breadcrumbs and log lines become Crashlytics log lines, errors become