npm.io
13.3.0 • Published 14h ago

@oxyhq/services

Licence
MIT
Version
13.3.0
Deps
1
Size
19.8 MB
Vulns
0
Weekly
2.3K

@oxyhq/services

A comprehensive TypeScript UI library for the Oxy API providing authentication, user management, and UI components for React Native and Expo applications.

Current published version: 6.10.6

For web apps (Vite, Next.js, CRA): Use @oxyhq/auth for authentication and @oxyhq/core for types and services.

For backend / Node.js: Use @oxyhq/core only.

For the full platform guide: See PLATFORM_GUIDE.md.

Table of Contents

Features

  • Zero-Config Authentication: Automatic token management and refresh
  • Cross-Domain SSO: Sign in once, authenticated everywhere (FedCM, popup, redirect)
  • Universal Provider: Single OxyProvider works on iOS, Android, and Expo Web
  • UI Components: Pre-built components for auth, profiles, and more
  • Inter Font Included: Default Oxy ecosystem font with automatic loading
  • Cross-Platform: Works in Expo and React Native (iOS, Android, Web)
  • Multi-Session Support: Manage multiple user sessions simultaneously
  • TypeScript First: Full type safety and IntelliSense support
  • Performance Optimized: Automatic caching with TanStack Query
  • Production Ready: Error handling, retry logic, and security best practices

Architecture

The OxyHQ SDK is split into three packages:

Package Use Case Dependencies
@oxyhq/services Expo / React Native apps Full (RN, Expo)
@oxyhq/auth Web apps (Vite, Next.js) React only
@oxyhq/core All platforms (types, API client, crypto) None

This package (@oxyhq/services) is for Expo and React Native applications. It provides OxyProvider, UI components, screens, bottom sheet routing, fonts, and hooks.

See PLATFORM_GUIDE.md for the complete architecture guide.

Installation

bun add @oxyhq/services @oxyhq/core
Peer Dependencies

To avoid duplicate native modules and ensure smooth integration across apps, install (or ensure your app already includes) the following peer dependencies:

  • react: >=18, react-native: >=0.76
  • react-native-reanimated: >=3.16, react-native-gesture-handler: >=2.16
  • react-native-safe-area-context: ^5.4.0, react-native-svg: >=13
  • Expo projects: expo, expo-font, expo-image, expo-linear-gradient
  • Navigation (if you use the provided screens): @react-navigation/native

Example for Expo:

npm i react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-svg \
  expo expo-font expo-image expo-linear-gradient @react-navigation/native
React Native/Expo Setup

For React Native and Expo projects, add the polyfill import at the very top of your entry file:

// index.js or App.js (very first line)
import 'react-native-url-polyfill/auto';

Note: This polyfill is already included in the package dependencies, but you need to import it to activate it.

Quick Start

Expo Apps (Native + Web)
import { OxyProvider, useAuth } from '@oxyhq/services';

function App() {
  return (
    <OxyProvider baseURL="https://api.oxy.so">
      <YourApp />
    </OxyProvider>
  );
}

function UserProfile() {
  const { user, isAuthenticated, signIn, signOut } = useAuth();

  if (!isAuthenticated) {
    return <Button onPress={() => signIn()} title="Sign In" />;
  }

  return (
    <View>
      <Text>Welcome, {user?.username}!</Text>
      <Button onPress={signOut} title="Sign Out" />
    </View>
  );
}

OxyProvider handles iOS, Android, and Expo web. Always use OxyProvider in Expo apps.

Typography - Inter Font

Inter is the default font for all Oxy ecosystem apps. This package includes the Inter font family and provides automatic font loading for both web and native platforms.

Automatic Loading

If you are using OxyProvider, fonts are loaded automatically. No additional setup needed:

import { OxyProvider } from '@oxyhq/services';

function App() {
  return (
    <OxyProvider baseURL="https://api.oxy.so">
      <YourAppContent />
    </OxyProvider>
  );
}
Using Font Constants
import { fontFamilies, fontStyles } from '@oxyhq/services';

const styles = StyleSheet.create({
  title: {
    ...fontStyles.titleLarge,  // Pre-defined style (54px, Bold)
    color: '#000000',
  },
  customText: {
    fontFamily: fontFamilies.interSemiBold,  // Cross-platform font family
    fontSize: 18,
  },
});
Available Exports
  • setupFonts() - Function to manually load fonts (called automatically by OxyProvider)
  • fontFamilies - Object with all Inter weight variants (inter, interLight, interMedium, interSemiBold, interBold, interExtraBold, interBlack)
  • fontStyles - Pre-defined text styles (titleLarge, titleMedium, titleSmall, buttonText)

See FONTS.md for the complete typography guide, including detailed usage examples, all available font weights, platform-specific handling, best practices, and migration guide.

Usage Patterns

OxyProvider works on all platforms (iOS, Android, Web). Use useAuth for authentication.

import { OxyProvider, useAuth, OxySignInButton } from '@oxyhq/services';

// App.tsx - Setup the provider
function App() {
  return (
    <OxyProvider
      baseURL="https://api.oxy.so"
      onAuthStateChange={(user) => {
        console.log('Auth state changed:', user ? 'logged in' : 'logged out');
      }}
    >
      <YourApp />
    </OxyProvider>
  );
}

// Component.tsx - Use the hook
function UserProfile() {
  const { user, isAuthenticated, signIn, signOut, isLoading } = useAuth();

  if (isLoading) return <ActivityIndicator />;

  if (!isAuthenticated) {
    return (
      <View>
        <Text>Welcome! Please sign in.</Text>
        <OxySignInButton />
        {/* Or use signIn() directly: */}
        <Button onPress={() => signIn()} title="Sign In" />
      </View>
    );
  }

  return (
    <View>
      <Text style={styles.title}>Welcome, {user?.username}!</Text>
      <Button onPress={signOut} title="Sign Out" />
    </View>
  );
}
2. Direct Import (Non-React Files)

For utility functions, services, or non-React Native files:

import { oxyClient } from '@oxyhq/core';

// utils/api.ts
export const userUtils = {
  async fetchUserById(userId: string) {
    return await oxyClient.getUserById(userId);
  },

  async fetchProfileByUsername(username: string) {
    return await oxyClient.getProfileByUsername(username);
  },

  async updateUserProfile(updates: any) {
    return await oxyClient.updateProfile(updates);
  }
};
3. Mixed Usage (Hooks + Direct Client)

You can use both hooks and the direct client in the same Expo app:

// App.tsx - React Native setup
import { OxyProvider } from '@oxyhq/services';

function App() {
  return (
    <OxyProvider baseURL="https://cloud.oxy.so">
      <YourApp />
    </OxyProvider>
  );
}

// utils/api.ts - Direct import
import { oxyClient } from '@oxyhq/core';

export const apiUtils = {
  async fetchData() {
    return await oxyClient.getCurrentUser();
  }
};

// Component.tsx - React Native hook
import { useOxy } from '@oxyhq/services';

function Component() {
  const { oxyServices } = useOxy();
  // Both oxyServices and oxyClient share the same tokens
}

API Reference

Core Exports (from @oxyhq/core)
import {
  OxyServices,           // Main service class
  oxyClient,            // Pre-configured instance
  OXY_CLOUD_URL,        // Default API URL
  OxyAuthenticationError,
  OxyAuthenticationTimeoutError,
  KeyManager,
  SignatureService,
  RecoveryPhraseService
} from '@oxyhq/core';
React Native Exports (from @oxyhq/services)
import {
  OxyProvider,          // Context provider
  useOxy,              // React Native hook
  useAuth,             // Auth hook
  OxySignInButton,     // UI components
  Avatar,
  FollowButton
} from '@oxyhq/services';
OxyServices Methods
// Authentication (Public Key Based)
await oxyClient.register(publicKey, username, signature, timestamp, email?);
await oxyClient.requestChallenge(publicKey);
await oxyClient.verifyChallenge(publicKey, challenge, signature, timestamp, deviceName?, deviceFingerprint?);
await oxyClient.checkPublicKeyRegistered(publicKey);
await oxyClient.getUserByPublicKey(publicKey);

// User Management
const user = await oxyClient.getCurrentUser();                    // Get current user
const userById = await oxyClient.getUserById('user123');          // Get user by ID
const profileByUsername = await oxyClient.getProfileByUsername('john_doe'); // Get profile by username
await oxyClient.updateProfile({ name: 'John Doe' });             // Update current user
await oxyClient.updateUser('user123', { name: 'John' });         // Update user by ID (admin)

// Session Management
const userBySession = await oxyClient.getUserBySession('session123'); // Get user by session
const sessions = await oxyClient.getSessionsBySessionId('session123'); // Get all sessions
await oxyClient.logoutSession('session123');                     // Logout specific session
await oxyClient.logoutAllSessions('session123');                 // Logout all sessions

// Social Features
await oxyClient.followUser('user123');                           // Follow user
await oxyClient.unfollowUser('user123');                         // Unfollow user
const followStatus = await oxyClient.getFollowStatus('user123'); // Check follow status
const followers = await oxyClient.getUserFollowers('user123');   // Get user followers
const following = await oxyClient.getUserFollowing('user123');   // Get user following

// Notifications
const notifications = await oxyClient.getNotifications();        // Get notifications
const unreadCount = await oxyClient.getUnreadCount();            // Get unread count
await oxyClient.markNotificationAsRead('notification123');       // Mark as read
await oxyClient.markAllNotificationsAsRead();                    // Mark all as read
await oxyClient.deleteNotification('notification123');           // Delete notification

// File Management
const fileData = await oxyClient.uploadFile(file);                     // Upload file
const file = await oxyClient.getFile('file123');                       // Get file info
await oxyClient.deleteFile('file123');                                 // Delete file
const downloadUrl = oxyClient.getFileDownloadUrl('file123', 'thumb'); // Get download/stream URL
const userFiles = await oxyClient.listUserFiles('user123');            // List user files

// Payments
const payment = await oxyClient.createPayment(paymentData);      // Create payment
const paymentInfo = await oxyClient.getPayment('payment123');    // Get payment info
const userPayments = await oxyClient.getUserPayments();          // Get user payments

// Trust
showBottomSheet('TrustCenter');                                  // Trust center
showBottomSheet('TrustLeaderboard');                             // Trust leaderboard

// Location Services
await oxyClient.updateLocation(40.7128, -74.0060);              // Update location
const nearby = await oxyClient.getNearbyUsers(1000);             // Get nearby users

// Analytics
await oxyClient.trackEvent('user_action', { action: 'click' });  // Track event
const analytics = await oxyClient.getAnalytics('2024-01-01', '2024-01-31'); // Get analytics

// Device Management
await oxyClient.registerDevice(deviceData);                      // Register device
const devices = await oxyClient.getUserDevices();                // Get user devices
await oxyClient.removeDevice('device123');                       // Remove device
const deviceSessions = await oxyClient.getDeviceSessions('session123'); // Get device sessions
await oxyClient.logoutAllDeviceSessions('session123');           // Logout device sessions
await oxyClient.updateDeviceName('session123', 'iPhone 15');     // Update device name

// Utilities
const metadata = await oxyClient.fetchLinkMetadata('https://example.com'); // Fetch link metadata
useOxy Hook
const {
  // Service instance
  oxyServices,

  // Authentication state
  user,
  isAuthenticated,
  isLoading,
  error,

  // Identity management (Public Key Authentication)
  createIdentity,     // Create new identity with recovery phrase
  importIdentity,     // Import identity from recovery phrase
  signIn,             // Sign in with stored identity
  hasIdentity,        // Check if identity exists on device
  getPublicKey,       // Get stored public key

  // Session management
  logout,

  // Session management
  sessions,
  activeSessionId,
  switchSession,
  removeSession
} = useOxy();

Configuration

OxyProvider Props
<OxyProvider
  baseURL="https://api.oxy.so"           // API base URL
  storageKeyPrefix="oxy_session"          // Storage key prefix
  onAuthStateChange={(user) => {}}        // Auth state callback
  onError={(error) => {}}                 // Error callback
>
  {children}
</OxyProvider>
Environment Variables
# .env
EXPO_PUBLIC_API_URL=https://api.oxy.so
Custom Configuration
import { OxyServices } from '@oxyhq/core';

const oxy = new OxyServices({
  baseURL: process.env.OXY_API_URL || 'https://cloud.oxy.so'
});

Authentication

Oxy supports public/private key cryptography (ECDSA secp256k1) as the primary identity system, with optional password-based accounts for the web gateway. Users manage their cryptographic identity in the Oxy Accounts app, and other apps can integrate "Sign in with Oxy" for seamless authentication.

Public Key Authentication
import { useOxy } from '@oxyhq/services';

function AuthScreen() {
  const { createIdentity, importIdentity, signIn, hasIdentity } = useOxy();

  // Create new identity (in Oxy Accounts app)
  const handleCreate = async () => {
    const { user, recoveryPhrase } = await createIdentity('username', 'email');
    // Show recoveryPhrase to user - they must save it!
  };

  // Import existing identity
  const handleImport = async (phrase: string) => {
    const user = await importIdentity(phrase, 'username', 'email');
  };

  // Sign in with stored identity
  const handleSignIn = async () => {
    const user = await signIn();
  };

  // Check if identity exists
  const hasStoredIdentity = await hasIdentity();
}
Password Authentication (Web Gateway)

For web gateway flows, Oxy also supports password sign-in (email/username + password). Use the auth gateway (/login, /signup, /recover) for browser-based flows, or call the API directly:

import { oxyClient } from '@oxyhq/core';

const session = await oxyClient.signUp('username', 'email@example.com', 'password');
const session2 = await oxyClient.signIn('username-or-email', 'password');
Cross-App Authentication (Sign in with Oxy)

For third-party apps that want to allow users to sign in with their Oxy identity:

import { OxySignInButton } from '@oxyhq/services';

function LoginScreen() {
  return <OxySignInButton variant="contained" />;
}

This displays:

  • A QR code that users can scan with Oxy Accounts
  • A button to open Oxy Accounts directly via deep link

Web fallback: send users to the auth gateway at https://accounts.oxy.so/authorize?token=... to approve the session.

Documentation

See Complete Public Key Authentication Guide for architecture, concepts, user flows, developer integration, crypto module API reference, security best practices, and migration from password auth.

UI Components

Built-in Components
import {
  OxySignInButton,
  Avatar,
  FollowButton,
  OxyLogo
} from '@oxyhq/services';

function MyComponent() {
  return (
    <View style={styles.container}>
      <OxyLogo />
      <Avatar userId="user123" size={40} />
      <FollowButton userId="user123" />
      <OxySignInButton />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
  },
});
Bottom Sheet Routing System

The bottom sheet routing system provides a clean, professional way to display authentication screens, account management, and other UI flows within a modal bottom sheet.

Quick Example:

import { useOxy } from '@oxyhq/services';

function MyComponent() {
  const { showBottomSheet } = useOxy();

  return (
    <Button
      onPress={() => showBottomSheet('OxyAuth')}
      title="Sign in with Oxy"
    />
  );
}

Features:

  • Full navigation history with back button support
  • Step-based screen navigation (multi-step flows)
  • Keyboard-aware (automatically adjusts for keyboard)
  • Dynamic sizing (fits content automatically)
  • Type-safe route names
  • 25+ pre-built screens available

Available Screens:

  • OxyAuth (Sign in with Oxy - for third-party apps)
  • ManageAccount, CreateManagedAccount
  • Profile, SessionManagement, PaymentGateway, TrustCenter
  • And many more...

Documentation: For complete documentation, see Bottom Sheet Routing Guide.

Using OxyRouter Standalone

If you need to embed the router in your own modal or container instead of using showBottomSheet():

import { Modal } from 'react-native';
import { useOxy, OxyRouter } from '@oxyhq/services';

function AuthModal({ visible, onRequestClose }: { visible: boolean; onRequestClose: () => void }) {
  const { oxyServices } = useOxy();

  if (!visible || !oxyServices) return null;

  return (
    <Modal visible onRequestClose={onRequestClose} animationType="slide">
      <OxyRouter
        oxyServices={oxyServices}
        initialScreen="OxyAuth"
        onClose={onRequestClose}
        onAuthenticated={onRequestClose}
        theme="light"
        containerWidth={360}
      />
    </Modal>
  );
}

Internationalization (i18n)

OxyHQ Services includes built-in language selection and storage. The selected language can be accessed and synced with your app's i18n system.

Getting Current Language
import { useOxy } from '@oxyhq/services';

function MyComponent() {
  const {
    currentLanguage,           // 'en-US'
    currentLanguageName,       // 'English'
    currentNativeLanguageName, // 'Espanol' (if Spanish is selected)
    currentLanguageMetadata    // Full metadata object
  } = useOxy();

  return <Text>Current language: {currentLanguageName}</Text>;
}
Syncing with Your i18n Library

To integrate with react-i18next, i18n-js, next-intl, or other i18n libraries, see the comprehensive guide:

See the language utilities and useOxy() hook sections above for integration with your i18n system.

Using OxyServices (Non-React)
import { OxyServices } from '@oxyhq/core';

const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });

// Get current language code
const languageCode = await oxy.getCurrentLanguage();

// Get language name
const languageName = await oxy.getCurrentLanguageName();

// Get full metadata
const metadata = await oxy.getCurrentLanguageMetadata();
Language Utilities
import {
  SUPPORTED_LANGUAGES,
  getLanguageName,
  getLanguageMetadata
} from '@oxyhq/services';

// Get all supported languages
const languages = SUPPORTED_LANGUAGES;

// Get language name from code
const name = getLanguageName('es-ES'); // 'Spanish'

// Get full metadata
const metadata = getLanguageMetadata('es-ES');
// { id: 'es-ES', name: 'Spanish', nativeName: 'Espanol', flag: '...', ... }

Troubleshooting

Common Issues
1. "useOxy must be used within an OxyContextProvider"

Solution: Wrap your app with OxyProvider

import { OxyProvider } from '@oxyhq/services';

function App() {
  return (
    <OxyProvider baseURL="https://cloud.oxy.so">
      <YourApp />
    </OxyProvider>
  );
}
2. FormData Issues in React Native/Expo

Solution: Add polyfill import at the very top of your entry file

// index.js or App.js (very first line)
import 'react-native-url-polyfill/auto';

Why needed: Your app uses file uploads which require FormData. React Native with Hermes engine does not include FormData natively, so it needs to be polyfilled.

3. Authentication Not Persisting

Solution: Check storage configuration

<OxyProvider
  baseURL="https://api.oxy.so"
  storageKeyPrefix="my_app_oxy"  // Custom storage key
>
  {children}
</OxyProvider>
Error Handling
import { OxyAuthenticationError } from '@oxyhq/core';

try {
  await oxyClient.getCurrentUser();
} catch (error) {
  if (error instanceof OxyAuthenticationError) {
    // Handle authentication errors
    console.log('Auth error:', error.message);
  } else {
    // Handle other errors
    console.log('Other error:', error.message);
  }
}

Payment & Storage Query Hooks

The following React Query hooks are exported from the package root:

import {
  useUserSubscription,
  useUserPayments,
  useUserWallet,
  useUserWalletTransactions,
  useAccountStorageUsage,
} from '@oxyhq/services';

Typed returns are defined in ui/hooks/queries/paymentTypes.ts (Subscription, Payment, Wallet, WalletTransaction). The payments query namespace is whitelisted for offline persistence alongside accounts, users, sessions, devices, and privacy.

Sign-In Token Planting

@oxyhq/core OxyServices.verifyChallenge() plants setTokens(accessToken, refreshToken ?? '') internally before returning. useAuthOperations.performSignIn no longer needs to hand-plant the token or call a session-token fallback — just await verifyChallenge and proceed.

Silent SSO Run-Once Guard

The cross-page-load deduplication guard lives in the consumer hooks, NOT in @oxyhq/core. A core module-level singleton was tried and reverted because it re-evaluates in the Metro web bundle and the guard did not hold. useWebSSO in this package owns a module-level silentSSOAttempted Set + ssoSignature(origin|baseURL) key for cross-mount deduplication, plus a per-instance hasCheckedRef fast-path. Do NOT move this guard into a core module-level singleton.

Requirements

  • React Native: 0.76+ (for mobile components)
  • Expo: 56+ (recommended)
  • TypeScript: 4.0+ (optional but recommended)
Peer Dependencies

For React Native/Expo projects:

bun add jwt-decode invariant

Note: react-native-url-polyfill is already included as a dependency in this package.

Examples

Complete React Native App
// App.tsx
import { OxyProvider } from '@oxyhq/services';

function App() {
  return (
    <OxyProvider baseURL="https://cloud.oxy.so">
      <UserDashboard />
    </OxyProvider>
  );
}

// UserDashboard.tsx
import { useOxy } from '@oxyhq/services';
import { View, Text, StyleSheet } from 'react-native';

function UserDashboard() {
  const { user, isAuthenticated, oxyServices } = useOxy();

  const [followers, setFollowers] = useState([]);

  useEffect(() => {
    if (isAuthenticated && user) {
      oxyServices.getUserFollowers(user.id).then(setFollowers);
    }
  }, [isAuthenticated, user]);

  if (!isAuthenticated) {
    return <Text>Please sign in</Text>;
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome, {user?.name}!</Text>
      <Text>Followers: {followers.length}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    justifyContent: 'center',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 16,
  },
});

Documentation

Comprehensive documentation is available in the /docs directory:


License

This project is licensed under the MIT License. See the LICENSE file for details.

Keywords