npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@webority/mobile-core

v0.15.0

Published

Platform layer for Webority React Native apps: HTTP clients, auth/token storage, config, logging, formatters, validators, network status, permissions, storage, version checks and over-the-air updates. No UI.

Readme

@webority/mobile-core

The platform layer for Webority React Native apps: HTTP clients, auth and token storage, config, logging, formatters, validators, network status, permissions, storage, image compression and version checks. No UI — components live in @webority/mobile-ui.

npm install @webority/mobile-core

Runtime dependencies: axios and libphonenumber-js. The required peers are expo, expo-application, expo-network, expo-secure-store (the HTTP client reads the bearer token from it) and expo-sqlite. Every other native peer is optional and reached only through its own import path below, so the package installs and runs without any of them. react-native-keychain is no longer used; token storage moved to expo-secure-store.

Full documentation lives in the repository's docs/ directory.

Import paths

Anything needing a native package lives behind its own path, so an app only bundles what it imports:

import { Permissions } from '@webority/mobile-core/permissions';
import { Geolocation } from '@webority/mobile-core/geolocation';
import { openDatabase } from '@webority/mobile-core/sqlite';
import { downloadFile } from '@webority/mobile-core/download';
import { shareFile } from '@webority/mobile-core/share';
import { requestReviewAfterSuccess } from '@webority/mobile-core/review';
import { initPush, enablePush, usePush } from '@webority/mobile-core/push';
import { definePushActionTask } from '@webority/mobile-core/push/task';
import { compressImage } from '@webority/mobile-core/compressor';
import { initOtaUpdates, useOtaUpdates } from '@webority/mobile-core/updates';
import { createApiBaseQuery, createAppApi, setupApiListeners } from '@webority/mobile-core/query';

Metro resolves requires statically, so anything reachable from the main barrel is bundled for every consumer. Keeping these off it is what makes their peers genuinely optional.

/query is the RTK Query layer; it needs @reduxjs/toolkit (2.6 or later) and react-redux, both optional peers. One createApi per app, built with createAppApi and the base query over the library's clients, so refresh, retry, the offline check and the extractApiError failure shape stay in one place. Never a fetchBaseQuery.

// services/api/apiSlice.ts
import { publicClient, userClient } from '@webority/mobile-core';
import { createApiBaseQuery, createAppApi, buildListQuery, providesPagedList,
  type PagedDataObject } from '@webority/mobile-core/query';

export const apiSlice = createAppApi({
  reducerPath: 'api',
  baseQuery: createApiBaseQuery({ client: userClient, publicClient }),
  tagTypes: ['Project'],
  endpoints: (build) => ({
    getProjects: build.query<PagedDataObject<Project>, { page: number }>({
      query: ({ page }) => ({ url: '/api/projects', params: buildListQuery({ page, pageSize: 20 }) }),
      providesTags: providesPagedList('Project')
    })
  })
});
// store.ts: add apiSlice.reducer and apiSlice.middleware, then setupApiListeners(store.dispatch)

setupApiListeners maps the app returning to the foreground (AppState) to RTK Query's focus, and this library's network status to online and offline; RTK's own defaults listen to window events that never fire on a device. A call marked silent: true, or an endpoint with extraOptions: { silent: true }, carries silent on action.meta.baseQueryMeta, which the error toast middleware skips.

/push registers the phone for push notifications. It needs expo-notifications, an optional peer, and reports unavailable without it or in Expo Go. Call initPush once at startup with getInstallationId, register, unregister and onOpen; enablePush asks permission, sends the native token (Fcm on Android, Apns on iOS) and the bundle id to register, and re-registers when the token changes; disablePush calls unregister and turns push off only once it succeeds (a failure rejects and leaves push on). resetPush turns push off on the phone with no server call and drops the native token, for a sign-out with no session left. onOpen receives the tapped notification's data as strings, including the tap that launched the app. usePush gives a settings switch its status and enabled. The library does not remember the switch: an app that keeps push on calls enablePush at each start. setBadgeCount(count) sets the app icon badge. dismissAllNotifications() clears every notification from the notification tray; it never throws, and logs a failure at warn.

  • enablePush({ prompt: false }) never shows the permission prompt: a phone already granted registers silently, and any other resolves with its status (undetermined when the person has not been asked yet, denied when refused and the prompt can still show, blocked when only Settings can turn it on). Call it at sign-in, and call enablePush() when the person reaches a moment where asking makes sense.
  • Every registered device carries apnsEnvironment: Development or Production on iOS, read from the build's aps-environment (expo-application), so the server sends each phone to the APNs gateway its signing accepts. It is null on Android, and on iOS when it cannot be read (the simulator); the server then uses its own setting.
  • onReceive(handler) calls handler({ notificationId, title, body, data }) for each notification that arrives while the app is open, shown or not, so the app can refresh a list or an unread count. It returns a function that removes the handler. A notification arriving in the background does not reach it; its tap still goes to onOpen.

Notification buttons

A push names a category; the category's buttons are registered on the phone. Register them at every start, with the same ids on both platforms:

import { setCategories, notifyLocal } from '@webority/mobile-core/push';

await setCategories([
  {
    id: 'approval',
    actions: [
      { id: 'approve', title: 'Approve', opensApp: false },
      { id: 'reject', title: 'Reject', destructive: true }
    ]
  },
  {
    id: 'reply',
    actions: [
      {
        id: 'reply',
        title: 'Reply',
        opensApp: false,
        textInput: { submitTitle: 'Send', placeholder: 'Write a reply' }
      }
    ]
  }
]);

opensApp defaults to true; destructive and authenticationRequired (unlock first) are iOS only. A button tap arrives as { actionId, userText, data, notificationId }: userText is the reply text, data the push data as strings, notificationId the notification's identifier on the phone. A plain tap still goes to onOpen; a button tap with no handler opens like a plain tap.

To receive button taps in every app state, the killed app included, call definePushActionTask once at module scope in the entry file, before the root component registers. It needs expo-task-manager, an optional peer (npx expo install expo-task-manager), so it lives on its own path:

// index.js
import { definePushActionTask } from '@webority/mobile-core/push/task';
import { notifyLocal } from '@webority/mobile-core/push';

definePushActionTask(async ({ actionId, userText, data }) => {
  try {
    await settle(actionId, userText, data);
  } catch {
    await notifyLocal({ title: 'Could not approve', body: 'Open the app to try again.' });
  }
});

The handler's promise is awaited, so its request can finish before the background work ends. onAction(handler) from /push sets the same handler for taps while JavaScript is running and returns a function that removes it. A response reported twice within ten seconds (the launch read, the listener and the Android task can each report it) runs once.

  • Android: the background notification task runs the handler for a button tap on a backgrounded or killed app. Buttons appear only on notifications expo-notifications draws (for example notifyLocal). An app that draws its own notifications natively from data-only pushes must build each button's intent with expo's NotificationsService.createNotificationResponseIntent, so the tap reaches this handler.
  • iOS: a button with opensApp: false launches the app in the background; the response reaches the listener definePushActionTask sets up. The task is not registered on iOS.

Storage and network status stay on the main barrel but load no package on their own. Wire them at startup with setStorageImplementation and setNetworkStatusImplementation if you want them.

Build number header and forced updates

Both HTTP clients send X-App-Build: <integer> on every request: the app's whole build number (VERSION_INTERNAL, Android versionCode, iOS CFBundleVersion). The API compares it with its minimum and answers an older build with 426 client_update_required. The number comes from initWebority({ appBuild }) (setConfig({ appBuild })), else from expo-application's nativeBuildVersion. Without a whole number the header is left off, and the server never refuses a request without it. X-Client-Build (<appVersion>+<commit>) is unchanged.

A 426 client_update_required opens the same force-update flow as the X-Min-App-Version response header: useVersionCheck turns isUpdateRequired on. The floor is the header when the reply has one, else the body's minVersion, else one above this build. A 426 is never retried.

Quiet requests and log redaction

Both HTTP clients log a failed response through Logger.error. For a call whose failure the app handles itself, pass skipErrorLog: true in the axios config and the clients stay silent:

await userClient.get('/status', { skipErrorLog: true });

Logger masks secrets in every message and argument before any sink sees them: Bearer tokens, JWT-shaped strings, and the values of keys named token, accessToken, refreshToken, password and authorization (any case) in objects and JSON strings.