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

@pafi-dev/app-ui

v0.4.0

Published

Mobile SDK for PAFI issuer app integration (React Native + Privy wallet)

Readme

@pafi-dev/app-ui

npm License: Apache-2.0

Mobile SDK for PAFI-integrated issuer apps. Provides a thin HTTP client, embedded Privy wallet management, and React hooks for React Native / Expo apps to integrate claim (mint), redeem (burn), and PAFI Web handoff flows.

Issuer app developers do not need to understand blockchain, wallets, or cryptographic signing — the SDK handles everything.

Platform support

| Platform | Supported | Notes | | --- | --- | --- | | iOS | ✅ | Requires Xcode 16+ | | Android | ✅ | Requires API 26+ | | Web | ❌ | Not supported — SDK depends on native Privy embedded wallet |

Requirements

  • Node.js ≥ 18
  • TypeScript ≥ 5.0
  • Expo SDK 52+ (SDK 54 recommended)
  • React Native ≥ 0.73.0

Install

pnpm add @pafi-dev/app-ui

Peer dependencies

Issuer apps must install the following as peer dependencies:

| Package | Version | Note | | --- | --- | --- | | react | ^18.0.0 \|\| ^19.0.0 | React 19 recommended for Expo 54+ | | react-native | >=0.73.0 | | | expo | >=52.0.0 | Expo SDK 54 recommended | | @privy-io/expo | ^0.64.0 \|\| ^0.65.0 | Wallet management + signing | | react-native-get-random-values | >=1.0.0 | Required polyfill for crypto.getRandomValues |

No web3 libraries required. The SDK does not depend on viem, ethers, permissionless, or any other blockchain library. The backend builds all UserOps and returns EIP-712 typed data; the mobile SDK signs it via eth_signTypedData_v4.

At a glance

import { PafiProvider, usePafiAuth, usePafiUser, usePafiClaim } from "@pafi-dev/app-ui";

// ── App root ──────────────────────────────────────────────────────
export default function App() {
  return (
    <PafiProvider
      baseUrl="https://api.example.com"
      getIssuerAccessToken={getIssuerAccessToken}
    >
      <Main />
    </PafiProvider>
  );
}

// ── Main screen ───────────────────────────────────────────────────
function Main() {
  const { login, isAuthenticated, walletAddress } = usePafiAuth();
  const { totalBalance, offChainBalance, onChainBalance, gasFeeUsdt } = usePafiUser();
  const { claim, isSubmitting } = usePafiClaim();

  if (!isAuthenticated) {
    return <Button onPress={login} title="Login" />;
  }

  return (
    <>
      <Text>Wallet: {walletAddress}</Text>
      <Text>Total Balance: {totalBalance} PT</Text>
      <Text>Claimable: {offChainBalance} PT</Text>
      <Text>On-chain: {onChainBalance} PT</Text>
      <Button
        onPress={() => claim({ amount: offChainBalance! })}
        title={isSubmitting ? "Claiming..." : "Claim"}
        disabled={isSubmitting}
      />
    </>
  );
}

That's the entire integration. No wallets, no UserOps, no signing.

What you get

| Export | Type | Description | | --- | --- | --- | | PafiProvider | Component | Root provider — wraps PrivyProvider + PafiClient context | | usePafiAuth | Hook | Login / logout with embedded Privy wallet + SIWE | | usePafiUser | Hook | Balances (off-chain, on-chain, total), pools, gas fee | | usePafiClaim | Hook | 2-step claim (mint): prepare → sign → submit | | usePafiRedeem | Hook | 2-step redeem (burn): prepare → sign → submit | | usePafiRedemptionPreview | Hook | v1.6 redemption policy preview + client-side validation | | usePafiDelegation | Hook | EIP-7702 delegation (handled automatically during claim/redeem) | | usePafiClaimStatus | Hook | Poll for claim (mint) transaction status | | usePafiRedeemStatus | Hook | Poll for redeem (burn) transaction status | | usePafiTransactions | Hook | Paginated transaction history | | usePafiEmailLink | Hook | Link email to Privy wallet (for PAFI Web handoff) | | usePafiWebHandoff | Hook | Open PAFI Web for swap / perp deposit | | useTos | Hook | Observe issuer-owned TOS acceptance state | | TosGate | Component | Conditionally renders children based on TOS acceptance | | PafiClient | Class | Thin HTTP client (use directly if not using React) | | PafiApiError | Class | Typed error for all backend responses | | PafiTosDeclinedError | Class | Thrown when user declines the TOS modal | | PafiTosRequiredError | Class | Thrown when claim/redeem is called before TOS acceptance |

The login flow

The issuer app calls login() — one function, no arguments. Internally:

login()
  ├── 1. Privy custom auth         → silent, returns privyUserId (no modal)
  ├── 2. TOS check / accept        → uses privyUserId, built-in modal if not yet accepted
  ├── 3. Wait for wallet ready     → reactive, max 30s timeout (creates embedded wallet)
  ├── 4. GET /auth/nonce           → backend returns random nonce
  ├── 5. buildSiweMessage(nonce)   → SDK builds EIP-4361 message
  ├── 6. wallet.signMessage(siwe)  → Privy signs (personal_sign)
  ├── 7. POST /auth/login          → backend verifies → JWT
  └── 8. client.setJwt(token)      → authenticated

TOS is checked before wallet creation because privyUserId is available immediately after Privy custom auth, while the embedded wallet is created asynchronously. This avoids blocking the user behind both flows simultaneously.

const { login, logout, isAuthenticated, isReady, walletAddress, error } = usePafiAuth();

// Block UI until Privy SDK is initialized
if (!isReady) return <ActivityIndicator />;

// One call — SDK handles everything
<Button onPress={login} title="Login" />

logout() calls POST /auth/logout to revoke the session server-side before clearing the local JWT and Privy session.

Terms of Service (TOS)

TOS is an issuer-owned legal state. The SDK ships a built-in modal and orchestrates the flow; the issuer backend is the source of truth for whether a user has accepted a given TOS version. Claim, redeem, and delegation actions throw PafiTosRequiredError until TOS is accepted.

The issuer backend must host two endpoints:

GET  /tos/status?userId={privyUserId}    → { accepted: boolean, version: string }
POST /tos/accept  { privyUserId, version } → { success: boolean }
<PafiProvider
  baseUrl="https://api.example.com"
  getIssuerAccessToken={getIssuerAccessToken}
  tos={{
    tosBaseUrl: "https://api.example.com",
    version: "v1.0",
    contentUrl: "https://api.example.com/legal/tos",
    title: "Terms of Service",
    branding: { accentColor: "#FF6B35" },
    onDeclined: async () => {
      // Optional issuer policy, e.g. navigate away.
    },
  }}
>
  <TosGate fallback={<ActivityIndicator />} blocked={<TosRequired />}>
    <PafiPoweredScreen />
  </TosGate>
</PafiProvider>

The TOS check runs automatically inside login() (after Privy custom auth, before wallet creation). If tos is omitted, the gate is disabled and all operations proceed without a TOS check.

const { isAccepted, isChecking, error, retry } = useTos();

The SDK does not hard-code any TOS copy or URL — contentUrl is owned by the issuer and rendered as a tappable link inside the built-in modal.

The claim flow

One-shot (no confirmation screen)

const { claim, isSubmitting, error } = usePafiClaim();

// One call — prepare + sign + submit
const { userOpHash } = await claim({ amount: "1000000000000000000" });

With confirmation screen

const { prepare, confirm, preparedData, isPreparing, isSubmitting } = usePafiClaim();

// Step 1 — get a summary for the UI
await prepare({ amount: "1000000000000000000" });

// Step 2 — show the user what they'll receive
// preparedData.summary: { amount, gasFee, netPtMinted, expiresAt }
<Text>Minting: {preparedData.summary.amount} PT</Text>
<Text>Gas fee: {preparedData.summary.gasFee} PT</Text>
<Text>Net: {preparedData.summary.netPtMinted} PT</Text>

// Step 3 — user confirms → SDK signs + submits
<Button onPress={() => confirm(preparedData.lockId)} title="Confirm" />

amount field

amount is a base-unit string (18 decimals). Pass offChainBalance from usePafiUser() to claim the full claimable balance:

const { offChainBalance } = usePafiUser();
claim({ amount: offChainBalance! });

The redeem flow

Redeem mirrors the claim flow — it burns on-chain PT and credits off-chain points.

One-shot

const { redeem, isSubmitting, error } = usePafiRedeem();

const { userOpHash } = await redeem({ amount: "1000000000000000000" });

With confirmation screen

const { prepare, confirm, preparedData, isPreparing, isSubmitting } = usePafiRedeem();

// Step 1 — prepare
await prepare({ amount: "1000000000000000000" });

// Step 2 — show summary
// preparedData.summary: { amount, gasFee, netPtBurned, expiresAt }
<Text>Burning: {preparedData.summary.amount} PT</Text>

// Step 3 — user confirms
<Button onPress={() => confirm(preparedData.lockId)} title="Confirm" />

Shortfall helper

For voucher redemption where the user may need to burn on-chain PT to cover a shortfall:

const { redeem, calculateShortfall } = usePafiRedeem();
const { offChainBalance } = usePafiUser();

const shortfall = calculateShortfall(voucherCostWei, offChainBalance!);
if (shortfall !== "0") {
  await redeem({ amount: shortfall });
}

Email linking

Email linking is deferred — users can view balances and claim immediately after login. Email is only linked when the user first needs PAFI Web access (e.g., Swap / Invest), and only once.

const { sendCode, confirmCode, status, isEmailLinked } = usePafiEmailLink();

// Gate swap behind email linking
const handleSwap = async () => {
  if (!isEmailLinked) {
    await sendCode(userEmail);   // Privy sends OTP
    showOtpModal();              // Show your OTP input UI
    return;
  }
  // Proceed to swap
};

// After user enters OTP
const handleOtpSubmit = async (code: string) => {
  await confirmCode({ code, email: userEmail });
  // Email linked — proceed to swap
};

| status | Meaning | | --- | --- | | idle | Not started | | sending | Sending OTP to the user's email | | awaiting_code | OTP sent, waiting for user input | | confirming | Verifying OTP code with Privy | | linked | Email successfully linked | | error | Something went wrong — check error |

Transaction history

Paginated transaction history with infinite scroll support:

const { transactions, isLoading, hasMore, loadMore, refresh } = usePafiTransactions();

// Render list
<FlatList
  data={transactions}
  renderItem={({ item }) => (
    <Text>{item.type}: {item.amount} PT — {item.status}</Text>
  )}
  onEndReached={() => hasMore && loadMore()}
  refreshing={isLoading}
  onRefresh={refresh}
/>

PAFI Web handoff

Open PAFI Web so the user can swap, deposit, or manage their assets. The SDK provides the URL — the issuer app decides when and how to open it.

const { openPafiWeb, webUrl } = usePafiWebHandoff();

// Open PAFI Web homepage
const url = await openPafiWeb();
await Linking.openURL(url);

Transaction polling

After a claim or redeem, the on-chain execution is asynchronous. You can use the dedicated status hooks to poll until the transaction reaches a terminal status:

import { usePafiClaimStatus } from "@pafi-dev/app-ui";

// lockId comes from the prepare() response, NOT userOpHash
const { status, txHash, isLoading, error } = usePafiClaimStatus(lockId);

if (status === "MINTED") {
  console.log("Claim successful! txHash:", txHash);
}

| Status | Meaning | | --- | --- | | PENDING | Tx submitted, awaiting on-chain confirmation | | MINTED | Mint event indexed — balance deducted from ledger | | EXPIRED | Prepare TTL expired before submit, or consent expired | | FAILED | On-chain tx reverted |

Error handling

All backend errors are wrapped in PafiApiError:

import { PafiApiError } from "@pafi-dev/app-ui";

try {
  await claim({ amount });
} catch (err) {
  if (err instanceof PafiApiError) {
    console.log(err.code);        // "INSUFFICIENT_BALANCE", "POLICY_CAP_EXCEEDED", …
    console.log(err.httpStatus);  // 400, 401, 422, 500, …
    console.log(err.safeToRetry); // false on claim errors — do NOT retry automatically
  }
}

Retry strategy (built into PafiClient)

| Condition | Action | | --- | --- | | 5xx + safeToRetry=true | Retry up to 3× with exponential backoff (500ms base, 5s cap, ±25% jitter) | | 5xx + safeToRetry=false | Never retry — tx may be in mempool | | 4xx | Never retry — client error | | 401 | Clear JWT + throw — app must call login() again | | Network error | Retry with same backoff (transient) — except /claim/submit, /redeem/submit, /delegate/submit: throw immediately (safeToRetry=false) |

PafiConfig reference

interface PafiConfig {
  /** Issuer backend URL. Captured at mount — see "Config immutability" below. */
  baseUrl: string;
  /** Issuer JWT callback for Privy custom auth */
  getIssuerAccessToken: () => Promise<string>;
  /** Chain ID (default: 8453 — Base mainnet) */
  chainId?: number;
  /** Point token contract address */
  pointTokenAddress?: string;
  /** PAFI Web URL for swap/invest handoff */
  pafiWebUrl?: string;
  /** Custom fetch for testing / RN polyfills. Captured at mount. */
  fetchFn?: typeof fetch;
  /** Wallet mode. Default is "embedded". Note: only "embedded" is supported in production at this time. */
  walletMode?: "embedded" | "external" | "both";
  /** Additional Privy config overrides */
  privyConfig?: Partial<PrivyClientConfig>;
  /** Optional issuer-owned TOS gate (built-in modal + Issuer BE source of truth) */
  tos?: TosConfig;
}

Config immutability

baseUrl and fetchFn are captured once at mount when PafiClient is constructed. Mutating them on the PafiProvider after mount has no effect — the underlying client keeps using the original values. Re-mount the provider (e.g., via key={baseUrl}) if you need to switch backends at runtime.

This is intentional: the JWT lifecycle is tied to a specific backend, and swapping baseUrl mid-session would silently leak the JWT to a different host.

Architecture constraints

Mobile never builds UserOps or batch calls. The backend is the single source of truth for UserOp construction. Mobile sends { amount }, receives EIP-712 typed data, the SDK signs it via eth_signTypedData_v4 internally, and the backend handles all blockchain complexity (UserOp assembly, paymaster, bundler submission).

No runtime dependency on blockchain packages. @pafi-dev/core, @pafi-dev/issuer, viem, and permissionless are NOT in dependencies. No ABI encoding, no RPC calls, no contract reads happen in the mobile bundle.

JWT is stored in memory only. The SDK does not persist the JWT to disk. If your app needs to survive process restarts without re-login, persist token from LoginResponse yourself (e.g. expo-secure-store) and call client.setJwt() on startup.

Exports

import {
  // Provider + context
  PafiProvider,
  type PafiProviderProps,
  usePafiClient,

  // Hooks
  usePafiAuth,
  type UsePafiAuthReturn,
  usePafiUser,
  type UsePafiUserReturn,
  usePafiClaim,
  type UsePafiClaimReturn,
  usePafiRedeem,
  type UsePafiRedeemReturn,
  usePafiRedemptionPreview,
  type UsePafiRedemptionPreviewReturn,
  usePafiDelegation,
  type UsePafiDelegationReturn,
  usePafiClaimStatus,
  type UsePafiClaimStatusReturn,
  usePafiRedeemStatus,
  type UsePafiRedeemStatusReturn,
  usePafiTransactions,
  type UsePafiTransactionsReturn,
  usePafiEmailLink,
  type UsePafiEmailLinkReturn,
  type EmailLinkStatus,
  usePafiWebHandoff,
  type UsePafiWebHandoffReturn,

  // TOS
  TosGate,
  useTos,
  PafiTosDeclinedError,
  PafiTosRequiredError,
  type TosConfig,
  type TosBrandingConfig,
  type TosStatus,
  type TosGateProps,
  type UseTosReturn,
  type TosStatusResponse,
  type TosAcceptResponse,

  // HTTP client
  PafiClient,
  PafiApiError,
  type PafiErrorType,
  type PafiErrorPayload,
  type PafiErrorMeta,
  type PafiErrorResponse,

  // Config types
  type PafiConfig,
  type PafiClientConfig,

  // Wire types — auth
  type NonceResponse,
  type LoginRequest,
  type LoginResponse,

  // Wire types — user
  type UserResponse,
  type PoolKey,
  type PoolsResponse,

  // Wire types — claim (mint)
  type PrepareClaimRequest,
  type PrepareClaimResponse,
  type ClaimSummary,
  type SubmitClaimRequest,
  type SubmitClaimResponse,
  type ClaimStatusResponse,

  // Wire types — redeem (burn)
  type PrepareRedeemRequest,
  type PrepareRedeemResponse,
  type RedeemSummary,
  type SubmitRedeemRequest,
  type SubmitRedeemResponse,
  type RedeemStatusResponse,

  // Wire types — redemption policy (v1.6)
  type RedemptionDenialCode,
  type RedemptionClientValidationCode,
  type RedemptionPreviewResponse,
  type RedemptionEvaluateRequest,
  type RedemptionEvaluateResponse,
  type RedemptionDenial,

  // Wire types — delegation
  type DelegationStatusResponse,
  type PrepareDelegationRequest,
  type PrepareDelegationResponse,
  type SubmitDelegationRequest,
  type SubmitDelegationResponse,

  // Wire types — web handoff
  type WebHandoffResponse,

  // Shared types
  type EIP712TypedData,
  type TransactionRecord,
  type TransactionsResponse,
} from "@pafi-dev/app-ui";

Tests

pnpm --filter @pafi-dev/app-ui test

All tests are hermetic — no network, no on-chain state required.

Changelog

See CHANGELOG.md for the authoritative changelog (current: 0.3.0 — Uniswap V3 migration + accumulated unreleased changes).

License

Apache-2.0