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

@sam-media-2/ouisys-dcb-widget

v2.0.0

Published

Standalone carrier-billing subscription-flow widget (PIN, MO, MO-redirect, one-click, click-to-SMS, USSD). Ships as a self-contained UMD bundle or importable React component.

Readme

@sam-media-2/ouisys-dcb-widget

A standalone, embeddable React widget that runs carrier-billing (DCB) subscription flows on mobile landing pages. Covers PIN, MO, MO-redirect, one-click (header enrichment), click-to-SMS, USSD, and operator-selection flows. Ships as a self-contained UMD bundle or importable React component.


Install

npm install @sam-media-2/ouisys-dcb-widget
# or
yarn add @sam-media-2/ouisys-dcb-widget

CDN (unpkg)

<!-- Latest -->
<script src="https://unpkg.com/@sam-media-2/ouisys-dcb-widget/embed/ouisys-dcb-widget.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@sam-media-2/ouisys-dcb-widget/embed/widget-base.css" />

<!-- Pinned version (recommended for production) -->
<script src="https://unpkg.com/@sam-media-2/[email protected]/embed/ouisys-dcb-widget.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@sam-media-2/[email protected]/embed/widget-base.css" />

Hosted on staging.mouisys.com

<script src="https://staging.mouisys.com/os-ui/static/eamobi/dcb/ouisys-dcb-widget.js"></script>
<link rel="stylesheet" href="https://staging.mouisys.com/os-ui/static/eamobi/dcb/widget-base.css" />

What it does

The widget owns exactly one thing: the subscription flow UI. It handles phone entry, PIN confirmation, MO SMS, one-click subscription, and the success terminal. Everything else — layout, design, copy, consent checkbox, surrounding page content — is the host page's responsibility.

The flow logic lives entirely inside ouisys-engine (a separate versioned package). The widget is its React UI layer: it renders the steps, hooks up the consent gate, fires events, and calls onSuccess when the flow completes.


Flows supported

| Flow | How it works | | --- | --- | | PIN | User enters phone → receives SMS PIN → enters PIN → subscribed | | MO | User enters phone → widget triggers MO SMS send → subscribed | | MO-redirect | User enters phone → redirected to payment URL → subscribed | | One-click | Carrier injects MSISDN via header enrichment → single button press → subscribed | | Click-to-SMS | User taps a button that opens the SMS app pre-filled with a keyword → subscribed | | USSD | User enters phone → USSD code displayed → dials to subscribe | | Operator selection | Widget shows a list of operators → user picks one → routes to that operator's flow |


Quick start

Plain HTML (most common)

<div id="subscribe"></div>
<link rel="stylesheet" href="https://unpkg.com/@sam-media-2/ouisys-dcb-widget/embed/widget-base.css" />
<script src="https://unpkg.com/@sam-media-2/ouisys-dcb-widget/embed/ouisys-dcb-widget.js"></script>

<script>
  window.OUISYS_COUNTRY = 'sa';
  window.pac_analytics = { visitor: { rockmanId: 'abc123', ip_range_name: 'sa', legals: [], chainRedirectUrl: '' } };
  window.configJson = {
    strategy: 'pin',
    country: 'sa',
    strategyConfigs: {
      default: { flow: 'pin', flowConfig: { host: '', slug: 'sa-mobily-gamezones', device: 'smart', country: 'SA', service: 'gamezones' } },
      operators: {},
      isDecryptMsisdnByApi: false,
      isExpectMsisdnInLocalHeaders: false
    },
    pageConfigs: { serviceName: 'GameZones', isShowConsentCheckBox: false, isDisableButtonIfInvalid: true, pin: { shortCodes: ['4242'] }, plan: { trialPrice: '2', isLocalCurrency: false } }
  };

  var cb = document.getElementById('consent');
  function p() {
    return {
      config: window.configJson, locale: 'en',
      consentValid: cb.checked,
      onEvent: function(name, payload) { analytics.track(name, payload); },
      onSuccess: function(url) { window.location.href = url; }
    };
  }
  var handle = OuisysSubscribe.mount('#subscribe', p());
  cb.addEventListener('change', function() { handle.update(p()); });
</script>

React

import { SubscriptionWidget } from '@sam-media-2/ouisys-dcb-widget/src';

<SubscriptionWidget
  config={configJson}
  locale="en"
  consentValid={consented}
  onEvent={(name, payload) => analytics.track(name, payload)}
  onSuccess={(url) => router.push(url)}
/>

Required globals

Three globals must be set on window before the widget loads. If any are missing the widget will silently fail or crash.

window.OUISYS_COUNTRY = 'sa';           // lowercase ISO2 country code

window.pac_analytics = {
  visitor: {
    rockmanId: 'abc123',                // any string — analytics/session ID
    ip_range_name: 'sa',               // drives IP-based operator detection
    legals: [],                        // ILegals[] — rendered in price points
    chainRedirectUrl: '',              // optional post-subscription redirect override
    // For one-click flows — the carrier injects this; mock it in dev:
    heMsisdnResult: { msisdn: '966512345678' }
  }
};

window.configJson = { ... };           // see Configuration below

Configuration — window.configJson

window.configJson = {
  strategy: 'pin',        // which flow to run — see strategy table below
  country: 'sa',          // lowercase ISO2

  strategyConfigs: {
    default: {
      flow: 'pin',        // camelCase flow name — DIFFERENT from strategy string
      flowConfig: {
        host: '',         // API host override; leave '' to use window.DEV_BASE_URL
        slug: 'sa-actel-mobily-gamezones',
        device: 'smart',  // 'smart' | 'feature'
        country: 'SA',    // UPPERCASE ISO2
        service: 'gamezones'
      }
    },
    operators: {},        // operator key → config (for ask-operator strategy)
    isDecryptMsisdnByApi: false,
    isExpectMsisdnInLocalHeaders: false
  },

  pageConfigs: {
    serviceName: 'GameZones',
    isShowConsentCheckBox: false,      // true = widget renders its own checkbox (unusual)
    isDisableButtonIfInvalid: true,
    pin: {
      shortCodes: ['4242'],            // shortcode shown in PIN step instructions
      blockedPin: ['0000', '1234']     // pins that open an alert dialog
    },
    plan: { trialPrice: '2', isLocalCurrency: false },
    hasMoButton: false
  }
};

Strategy name mapping

config.strategy and strategyConfigs.default.flow use different naming conventions.

| Intent | config.strategy | strategyConfigs.default.flow | | --- | --- | --- | | PIN flow | 'pin' | 'pin' | | MO flow | 'mo' | 'mo' | | MO redirect | 'mo-redir' | 'moRedir' | | One-click / header enrichment | 'header-enrichment' | 'oneClick' | | Click-to-SMS | 'click2sms' | 'click2sms' | | USSD | 'ussd' | 'ussd' | | Operator selection | 'ask-operator' | (per-operator in operators) | | Detect by IP | 'detect-operator-by-ip' | target flow (e.g. 'pin') |

⚠️ Never use camelCase in config.strategy — the engine matches hyphen-case strings exactly.


Widget props

| Prop | Type | Default | Purpose | | --- | --- | --- | --- | | config | WidgetConfig | window.configJson | Configuration object (see above) | | locale | string | 'en' | Locale for i18n | | messages | Record<string, string> | {} | Override any English label by message ID | | previewFlow | FlowName | — | Force a flow for design/QA (no backend needed) | | consentValid | boolean | undefined | false = widget buffers submit and emits ouisys.consent.required | | onConsentRequired | () => void | — | Legacy callback — fires alongside the event (useful for shake animations) | | onEvent | (name, payload) => void | — | Receives all widget events (analytics, custom consent, etc.) | | redact | boolean | false | Strip msisdn from all event payloads | | onSuccess | (productUrl: string) => void | — | Called at terminal success | | slots | Slots | — | Replace any step's markup with custom JSX | | className | string | — | Extra class on the widget root element |


MountHandle API (standalone embed)

OuisysSubscribe.mount(target, props) returns a handle with three methods:

| Method | Purpose | | --- | --- | | handle.update(props) | Re-render with new props — call on consent checkbox change | | handle.submit() | Replay the buffered pending action — call after granting consent | | handle.unmount() | Tear down the widget |


Host event system

The widget emits typed events at every meaningful moment through two simultaneous channels.

Channel 1 — onEvent callback

OuisysSubscribe.mount('#subscribe', {
  config: window.configJson,
  onEvent: function(name, payload) {
    myAnalytics.track(name, payload);
  }
});

Channel 2 — DOM CustomEvent

Events bubble up from the mount container with composed: true, so they work across shadow DOM boundaries.

document.querySelector('#subscribe').addEventListener('ouisys.subscription.success', function(e) {
  console.log(e.detail); // { msisdn: '...', productUrl: '...', flow: 'pin' }
});

MSISDN redaction

OuisysSubscribe.mount('#subscribe', { redact: true, onEvent: ... });

Strips msisdn from all payloads before they reach either channel.

Full event catalog

| Event | Payload | Fired when | | --- | --- | --- | | ouisys.widget.ready | flow | Strategy identified, first step visible | | ouisys.flow.start | — | Widget mounts, strategy identification begins | | ouisys.flow.error | error | Strategy identification failed | | ouisys.phone.submit | msisdn? | User submitted phone number | | ouisys.phone.success | msisdn? | Phone accepted, advancing to next step | | ouisys.phone.error | msisdn?, error | Phone rejected | | ouisys.pin.submit | msisdn? | User submitted PIN | | ouisys.pin.success | msisdn? | PIN verified | | ouisys.pin.error | msisdn?, error | PIN rejected (API failure or re-entry guard) | | ouisys.mo.submit | msisdn? | MO SMS triggered | | ouisys.mo.success | msisdn? | MO verified | | ouisys.mo.error | msisdn?, error | MO failed | | ouisys.oneclick.view | msisdn? | One-click button rendered | | ouisys.oneclick.submit | msisdn? | One-click button pressed | | ouisys.oneclick.success | msisdn?, productUrl | One-click subscription confirmed | | ouisys.oneclick.error | msisdn?, error | One-click failed | | ouisys.click2sms.view | keyword, shortcode | Click-to-SMS instructions rendered | | ouisys.click2sms.submit | keyword, shortcode, msisdn? | SMS button pressed | | ouisys.ussd.submit | msisdn? | USSD phone-entry submitted | | ouisys.ussd.success | msisdn?, keyword, shortcode | USSD code displayed | | ouisys.ussd.error | msisdn?, error | USSD failed | | ouisys.operator.select | operator | Operator chosen on selection screen | | ouisys.consent.required | pendingAction | Submit attempted while consentValid is false | | ouisys.subscription.success | msisdn?, productUrl, flow | Terminal success on any flow | | ouisys.subscription.error | msisdn?, error, flow | Terminal error on any flow |

msisdn? is omitted when redact: true. error is always a string. flow is camelCase: 'pin', 'mo', 'moRedir', 'oneClick', 'click2sms', 'ussd'.

Dev-mode console logger

In NODE_ENV=development, every event is automatically logged via console.groupCollapsed — no configuration needed. Respects redact: true. Stripped from the production bundle entirely.


Consent handling

The page owns the consent UI. The widget enforces it.

Standard pattern — checkbox + shake animation

var cb  = document.getElementById('consent');
var row = document.getElementById('consent-row');

function props() {
  return {
    config: window.configJson, locale: 'en',
    consentValid: cb.checked,
    onConsentRequired: function() {
      row.classList.remove('shake');
      void row.offsetWidth; // reflow to restart animation
      row.classList.add('shake');
    },
    onSuccess: function(url) { window.location.href = url; }
  };
}

var handle = OuisysSubscribe.mount('#subscribe', props());
cb.addEventListener('change', function() { handle.update(props()); });

Advanced pattern — consent modal with handle.submit()

Use when consent requires a modal, async T&C acceptance, or multi-step flow. The widget buffers the pending submit (phone or PIN) and replays it after consent is granted.

var handle = OuisysSubscribe.mount('#subscribe', {
  config: window.configJson,
  locale: 'en',
  consentValid: false,
  onEvent: function(name, payload) {
    if (name === 'ouisys.consent.required') {
      // payload.pendingAction → 'phone' | 'pin' | 'mo' | 'oneclick'
      showConsentModal({
        onAccept: function() {
          handle.update({ config: window.configJson, consentValid: true });
          handle.submit(); // replays the buffered action seamlessly
        }
      });
    }
  },
  onSuccess: function(url) { window.location.href = url; }
});

Both patterns can coexist — onConsentRequired still fires alongside ouisys.consent.required.


Customization

Level 1 — CSS token override (fastest)

Load the base stylesheet and override variables on the mount container:

<link rel="stylesheet" href="widget-base.css" />
<style>
  #subscribe {
    --ow-accent: #6d28d9;
    --ow-accent-hover: #5b21b6;
    --ow-radius: 14px;
    --ow-font: "Inter", sans-serif;
  }
</style>

| Token | Purpose | Default | | --- | --- | --- | | --ow-accent | Primary CTA background | #1f7a4d | | --ow-accent-hover | CTA hover state | #155e3a | | --ow-accent-contrast | Text on CTA | #ffffff | | --ow-bg | Widget surface background | transparent | | --ow-text | Body text | #1a1a1a | | --ow-muted | Secondary text | #667085 | | --ow-border | Input borders | #cfd8e0 | | --ow-error | Error text | #c0392b | | --ow-radius | Corner radius | 10px | | --ow-gap | Vertical rhythm | 12px | | --ow-font | Font stack | system-ui | | --ow-font-size | Base font size | 16px | | --ow-control-height | Input/button height | 48px | | --ow-disabled | Disabled button background | #aebfcb |

Level 2 — Class hook CSS

Skip widget-base.css and write your own CSS against the widget's stable class hooks.

Key hooks:

.ouisys-subscription-widget    widget root
.flow-pin / .flow-mo / .flow-moredir / .flow-oneclick / .flow-click2sms / .flow-ussd
.phone-entry-step  .text-input  .btn  .flag  .country-code
.pin-entry-form  .pin-input  .pinEntryLabel  .wrongPinMessage  .link-try-again
.mo__instruction  .mo-link-verify-sms
.tq-step  .error-msg  .label

Level 3 — Slot overrides

Replace any step's entire markup with your own JSX:

<SubscriptionWidget
  config={config}
  slots={{
    phone: ({ submitMsisdn }) => <MyPhoneForm onSubmit={submitMsisdn} />,
    thankYou: ({ finalUrl }) => <MySuccessScreen href={finalUrl} />,
  }}
/>

Available slots: phone, pin, mo, moRedir, oneClick, click2sms, ussd, operatorSelect, loader, thankYou.


Localization

Override any English default by passing messages:

OuisysSubscribe.mount('#subscribe', {
  config: window.configJson,
  locale: 'ar',
  messages: {
    msisdnLabel: 'أدخل رقم هاتفك',
    msisdnButton: 'متابعة',
    pinLabel: 'أدخل رمز PIN',
    pinButton: 'تأكيد',
    congratsTitle: 'تهانينا!',
    congratsText: 'لقد اشتركت بنجاح في الخدمة.'
  }
});

All message IDs are in src/messages.ts.


Preview and QA

Preview any flow without a backend — no carrier or API needed.

yarn demo
# Open http://localhost:8090/?ui_flow=pin

Available ui_flow values: pin, mo, moRedir, oneClick, click2sms, ussd, operatorSelect, loader.

Add &ui_step=phone or &ui_step=thankyou to jump to a specific step.

Or force a flow in code:

<SubscriptionWidget config={config} previewFlow="oneClick" />

When a flow is forced, the engine's strategy identification is skipped entirely.


Build and publish

yarn dev            # live-reload dev server with real engine and backend
yarn demo           # all flows, no backend
yarn build:widget   # → embed/ouisys-dcb-widget.js (UMD, self-contained)
yarn test           # Jest suite
yarn test:watch     # watch mode

Publishing to npm

# Log in to npm under the @sam-media-2 org
npm login

# Dry run — see exactly what will be published
npm publish --dry-run

# Publish
npm publish

prepublishOnly runs yarn build:widget automatically before every publish, so the bundle is always up to date.

Deploying the hosted version (S3 / staging.mouisys.com)

yarn deploy:embed   # build:widget then upload to S3
yarn upload:embed   # upload only (skips build — uses existing dist/)

Requires osui_aws_access_key_id and osui_secret_access_key in the environment.

Uploads to s3://mobirun/os-ui/static/eamobi/dcb/https://staging.mouisys.com/os-ui/static/eamobi/dcb/.

Every deploy publishes three JS artifacts:

| File | Cache | Purpose | | --- | --- | --- | | ouisys-dcb-widget.js | max-age=60 | Stable URL — safe to hard-code in landing pages | | ouisys-dcb-widget.<hash>.js | immutable | Content-hashed — guaranteed exact build | | embed-manifest.json | no-store | Always-fresh JSON pointer to the current hashed URL |

Using embed-manifest.json

fetch('https://staging.mouisys.com/os-ui/static/eamobi/dcb/embed-manifest.json')
  .then(r => r.json())
  .then(function(manifest) {
    var s = document.createElement('script');
    s.src = manifest.src; // immutable hashed URL — zero CDN lag
    document.head.appendChild(s);
  });

Testing

Tests live in src/__tests__/. The entire ouisys-engine is replaced with a controllable mock (src/test/ouisysEngineMock.tsx) — all widget providers, context, event emitter, and UI render for real.

import { __setFlowState, __setStrategyState, rdsNotAsked, rdsFailure } from '../test/ouisysEngineMock';

// Put the widget in PIN entry step
__setFlowState('pin', { __branch: 'pinEntry', rds: rdsNotAsked(), nextData: { operator: 'SA_MOBILY' } });

// Assert events
const onEvent = jest.fn();
render(<SubscriptionWidget config={config} onEvent={onEvent} />);
expect(onEvent).toHaveBeenCalledWith('ouisys.phone.success', { msisdn: '966512345678' });

Source layout

src/
├── SubscriptionWidget.tsx     root component — strategy switch + lifecycle events
├── embed.tsx                  UMD entry — OuisysSubscribe.mount / update / submit / unmount
├── index.tsx                  React package entry
├── types.ts                   WidgetConfig, Slots, SubscriptionWidgetProps, PendingAction
├── events.ts                  createEmitter(el, onEvent?, redact?) → EmitFn; WidgetEventName
├── messages.ts                English default labels + all message IDs
├── slots.ts                   resolveSlot() helper
├── providers/
│   ├── WidgetContext.tsx       context: config, msisdn, consent, emit, pendingAction/Replay
│   ├── store.ts               Redux store (engine reducers)
│   └── intl.tsx               react-intl provider with message merging
├── flows/                     one component per flow; each owns consent intercept + event emission
├── steps/                     PhoneEntryStep, PINEntryStep, ThankYouStep, etc.
├── ui/                        Button, PhoneInput, PinEntryForm, PricePoint, etc.
├── demo/
│   ├── main.tsx               yarn dev entry (real engine, live backend)
│   └── demoEngine.tsx         yarn demo mock (all flows, no backend)
└── __tests__/                 Jest + RTL tests for every flow and step

Common gotchas

Unexpected state type: undefinedsetConfig race condition. WidgetContext.tsx uses useLayoutEffect (not useEffect) to guarantee config is set before identifyStrategy reads it. Never change this back to useEffect.

Strategy not supportedconfig.strategy is in camelCase (e.g. 'oneClick'). The engine only accepts hyphen-case. See the strategy mapping table above.

handle.submit() does nothingsubmit() replays a buffered pending action. There is no buffered action until the user presses submit with consentValid: false. Only call submit() from your consent modal's accept handler, after ouisys.consent.required has fired.

PIN error not fired on re-entry — Re-submitting a previously-failed PIN triggers a local re-entry guard in PinEntryForm before the engine is involved. The event fires as ouisys.pin.error with error: 'WrongPinReEntry'.

One-click button never appearspac_analytics.visitor.heMsisdnResult.msisdn must be set. The carrier injects this on a live data connection. In dev, mock it manually.

Try-again link not visible after PIN failurePINEntryStep renders .link-try-again in both nothingYet and failure RDS states. It is hidden only during loading and after success.