@idealyst/mcp-server 1.2.106 → 1.2.108

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.
package/dist/index.cjs CHANGED
@@ -5245,6 +5245,7 @@ Cross-platform storage solution for React and React Native applications. Provide
5245
5245
  - **React Native** - Uses MMKV for high-performance storage
5246
5246
  - **Web** - Uses localStorage with proper error handling
5247
5247
  - **TypeScript** - Full type safety and IntelliSense support
5248
+ - **Secure Storage** - Optional encrypted storage via \`createSecureStorage()\` (Keychain on native, Web Crypto on web)
5248
5249
 
5249
5250
  ## Installation
5250
5251
 
@@ -5648,6 +5649,127 @@ async function safeStorageOperation() {
5648
5649
  6. **Data Size** - Keep stored objects reasonably sized
5649
5650
  7. **Cleanup** - Periodically clean up unused data
5650
5651
  8. **Type Safety** - Create typed wrapper functions for better TypeScript support
5652
+ 9. **Use Secure Storage for Secrets** - Use \`createSecureStorage()\` for auth tokens, API keys, and sensitive data
5653
+ `,
5654
+ "idealyst://storage/secure": `# Secure Storage
5655
+
5656
+ Encrypted storage for sensitive data like auth tokens, API keys, and secrets. Uses the same \`IStorage\` interface as regular storage \u2014 drop-in replacement.
5657
+
5658
+ ## Installation
5659
+
5660
+ \`\`\`bash
5661
+ yarn add @idealyst/storage
5662
+
5663
+ # React Native also needs (for secure storage):
5664
+ yarn add react-native-keychain react-native-mmkv
5665
+ cd ios && pod install
5666
+ \`\`\`
5667
+
5668
+ ## Quick Start
5669
+
5670
+ \`\`\`tsx
5671
+ import { createSecureStorage } from '@idealyst/storage';
5672
+
5673
+ // Create a secure storage instance
5674
+ const secureStorage = createSecureStorage();
5675
+
5676
+ // Same API as regular storage
5677
+ await secureStorage.setItem('authToken', 'eyJhbGciOiJIUzI1NiIs...');
5678
+ const token = await secureStorage.getItem('authToken');
5679
+ await secureStorage.removeItem('authToken');
5680
+ await secureStorage.clear();
5681
+ const keys = await secureStorage.getAllKeys();
5682
+
5683
+ // Listeners work too
5684
+ const unsubscribe = secureStorage.addListener((key, value) => {
5685
+ console.log('Secure storage changed:', key);
5686
+ });
5687
+ \`\`\`
5688
+
5689
+ ## Options
5690
+
5691
+ \`\`\`tsx
5692
+ import { createSecureStorage, SecureStorageOptions } from '@idealyst/storage';
5693
+
5694
+ const secureStorage = createSecureStorage({
5695
+ prefix: 'myapp', // Namespace for keys (default: 'secure')
5696
+ });
5697
+ \`\`\`
5698
+
5699
+ The \`prefix\` option controls:
5700
+ - **Native**: Keychain service name and MMKV instance ID
5701
+ - **Web**: localStorage key prefix and IndexedDB key name
5702
+
5703
+ Use different prefixes to create isolated secure storage instances.
5704
+
5705
+ ## How It Works
5706
+
5707
+ ### React Native
5708
+ 1. A random 16-byte encryption key is generated on first use
5709
+ 2. The key is stored in the **iOS Keychain** / **Android Keystore** (hardware-backed)
5710
+ 3. An encrypted MMKV instance is created using that key
5711
+ 4. All data is encrypted at rest by MMKV's native AES encryption
5712
+ 5. Keychain accessibility is set to \`WHEN_UNLOCKED_THIS_DEVICE_ONLY\` (not backed up, only accessible when device is unlocked)
5713
+
5714
+ ### Web
5715
+ 1. A non-extractable AES-256-GCM \`CryptoKey\` is generated on first use
5716
+ 2. The key is stored in **IndexedDB** (non-extractable \u2014 cannot be read as raw bytes)
5717
+ 3. Each value is encrypted with a unique random IV before storing in localStorage
5718
+ 4. Requires a **secure context** (HTTPS) for \`crypto.subtle\` access
5719
+
5720
+ ## Usage Example: Secure Auth Service
5721
+
5722
+ \`\`\`tsx
5723
+ import { createSecureStorage } from '@idealyst/storage';
5724
+
5725
+ const secureStorage = createSecureStorage({ prefix: 'auth' });
5726
+
5727
+ interface AuthTokens {
5728
+ accessToken: string;
5729
+ refreshToken: string;
5730
+ expiresAt: number;
5731
+ }
5732
+
5733
+ class SecureAuthService {
5734
+ static async saveTokens(tokens: AuthTokens) {
5735
+ await secureStorage.setItem('tokens', JSON.stringify(tokens));
5736
+ }
5737
+
5738
+ static async getTokens(): Promise<AuthTokens | null> {
5739
+ const data = await secureStorage.getItem('tokens');
5740
+ return data ? JSON.parse(data) as AuthTokens : null;
5741
+ }
5742
+
5743
+ static async clearTokens() {
5744
+ await secureStorage.removeItem('tokens');
5745
+ }
5746
+
5747
+ static async saveApiKey(key: string) {
5748
+ await secureStorage.setItem('apiKey', key);
5749
+ }
5750
+
5751
+ static async getApiKey(): Promise<string | null> {
5752
+ return secureStorage.getItem('apiKey');
5753
+ }
5754
+ }
5755
+ \`\`\`
5756
+
5757
+ ## When to Use Secure vs Regular Storage
5758
+
5759
+ | Data Type | Use |
5760
+ |-----------|-----|
5761
+ | Auth tokens, refresh tokens | \`createSecureStorage()\` |
5762
+ | API keys, client secrets | \`createSecureStorage()\` |
5763
+ | User preferences, theme | \`storage\` (regular) |
5764
+ | Cache data | \`storage\` (regular) |
5765
+ | Session IDs | \`createSecureStorage()\` |
5766
+ | Language preference | \`storage\` (regular) |
5767
+
5768
+ ## Platform Requirements
5769
+
5770
+ - **React Native**: Requires \`react-native-keychain\` (>=9.0.0) and \`react-native-mmkv\` (>=4.0.0)
5771
+ - **Web**: Requires secure context (HTTPS) and IndexedDB support
5772
+ - **Web limitation**: IndexedDB may not be available in some private browsing modes
5651
5773
  `
5652
5774
  };
5653
5775
 
@@ -6899,6 +7021,156 @@ function UploadForm() {
6899
7021
  "Use customMimeTypes for specific formats like 'application/pdf'"
6900
7022
  ],
6901
7023
  relatedPackages: ["components", "camera", "storage"]
7024
+ },
7025
+ clipboard: {
7026
+ name: "Clipboard",
7027
+ npmName: "@idealyst/clipboard",
7028
+ description: "Cross-platform clipboard and OTP autofill for React and React Native. Copy/paste text and auto-detect SMS verification codes on mobile.",
7029
+ category: "utility",
7030
+ platforms: ["web", "native"],
7031
+ documentationStatus: "full",
7032
+ installation: "yarn add @idealyst/clipboard",
7033
+ peerDependencies: [
7034
+ "@react-native-clipboard/clipboard (native)",
7035
+ "react-native-otp-verify (Android OTP, optional)"
7036
+ ],
7037
+ features: [
7038
+ "Cross-platform copy/paste with async API",
7039
+ "Android SMS OTP auto-read via SMS Retriever API (no permissions)",
7040
+ "iOS OTP keyboard autofill via TextInput props",
7041
+ "Clipboard change listeners",
7042
+ "Graceful degradation when native modules missing"
7043
+ ],
7044
+ quickStart: `import { clipboard } from '@idealyst/clipboard';
7045
+
7046
+ // Copy text
7047
+ await clipboard.copy('Hello, world!');
7048
+
7049
+ // Paste text
7050
+ const text = await clipboard.paste();
7051
+
7052
+ // OTP auto-fill (mobile)
7053
+ import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
7054
+
7055
+ function OTPScreen() {
7056
+ const { code, startListening } = useOTPAutoFill({
7057
+ codeLength: 6,
7058
+ onCodeReceived: (otp) => verifyOTP(otp),
7059
+ });
7060
+
7061
+ useEffect(() => { startListening(); }, []);
7062
+
7063
+ return <TextInput value={code ?? ''} {...OTP_INPUT_PROPS} />;
7064
+ }`,
7065
+ apiHighlights: [
7066
+ "clipboard.copy(text) - Copy text to clipboard",
7067
+ "clipboard.paste() - Read text from clipboard",
7068
+ "clipboard.hasText() - Check if clipboard has text",
7069
+ "clipboard.addListener(fn) - Listen for copy events",
7070
+ "useOTPAutoFill({ codeLength, onCodeReceived }) - Auto-detect SMS OTP codes (Android)",
7071
+ "OTP_INPUT_PROPS - TextInput props for iOS OTP keyboard autofill"
7072
+ ],
7073
+ relatedPackages: ["storage", "components"]
7074
+ },
7075
+ biometrics: {
7076
+ name: "Biometrics",
7077
+ npmName: "@idealyst/biometrics",
7078
+ description: "Cross-platform biometric authentication and passkeys (WebAuthn/FIDO2) for React and React Native. FaceID, TouchID, fingerprint, iris, and passwordless login.",
7079
+ category: "auth",
7080
+ platforms: ["web", "native"],
7081
+ documentationStatus: "full",
7082
+ installation: "yarn add @idealyst/biometrics",
7083
+ peerDependencies: [
7084
+ "expo-local-authentication (native biometrics)",
7085
+ "react-native-passkeys (native passkeys, optional)"
7086
+ ],
7087
+ features: [
7088
+ "Local biometric auth \u2014 FaceID, TouchID, fingerprint, iris",
7089
+ "Passkeys (WebAuthn/FIDO2) \u2014 passwordless login with cryptographic credentials",
7090
+ "Cross-platform \u2014 single API for web and React Native",
7091
+ "Web biometrics via WebAuthn userVerification",
7092
+ "Web passkeys via navigator.credentials",
7093
+ "Graceful degradation when native modules missing",
7094
+ "Base64url encoding helpers for WebAuthn data"
7095
+ ],
7096
+ quickStart: `import { isBiometricAvailable, authenticate } from '@idealyst/biometrics';
7097
+
7098
+ // Check biometric availability
7099
+ const available = await isBiometricAvailable();
7100
+
7101
+ // Authenticate
7102
+ if (available) {
7103
+ const result = await authenticate({ promptMessage: 'Verify your identity' });
7104
+ if (result.success) {
7105
+ // Authenticated!
7106
+ }
7107
+ }
7108
+
7109
+ // Passkeys
7110
+ import { isPasskeySupported, createPasskey, getPasskey } from '@idealyst/biometrics';
7111
+
7112
+ const credential = await createPasskey({
7113
+ challenge: serverChallenge,
7114
+ rp: { id: 'example.com', name: 'My App' },
7115
+ user: { id: userId, name: email, displayName: name },
7116
+ });`,
7117
+ apiHighlights: [
7118
+ "isBiometricAvailable() - Check biometric hardware and enrollment",
7119
+ "getBiometricTypes() - Get available biometric types",
7120
+ "getSecurityLevel() - Get device security level",
7121
+ "authenticate(options) - Prompt for biometric auth",
7122
+ "cancelAuthentication() - Cancel auth (Android)",
7123
+ "isPasskeySupported() - Check passkey support",
7124
+ "createPasskey(options) - Register a new passkey",
7125
+ "getPasskey(options) - Authenticate with passkey"
7126
+ ],
7127
+ relatedPackages: ["oauth-client", "storage"]
7128
+ },
7129
+ payments: {
7130
+ name: "Payments",
7131
+ npmName: "@idealyst/payments",
7132
+ description: "Cross-platform payment provider abstractions for React and React Native. Wraps Stripe Platform Pay API for Apple Pay and Google Pay on mobile. Web provides a stub directing to Stripe Elements.",
7133
+ category: "utility",
7134
+ platforms: ["web", "native"],
7135
+ documentationStatus: "full",
7136
+ installation: "yarn add @idealyst/payments @stripe/stripe-react-native",
7137
+ peerDependencies: [
7138
+ "@stripe/stripe-react-native (native)"
7139
+ ],
7140
+ features: [
7141
+ "Apple Pay via Stripe Platform Pay (iOS)",
7142
+ "Google Pay via Stripe Platform Pay (Android)",
7143
+ "Flat function API + usePayments convenience hook",
7144
+ "PaymentIntent confirmation flow",
7145
+ "Payment method creation flow",
7146
+ "Normalized error handling across platforms",
7147
+ "Graceful degradation when Stripe SDK missing",
7148
+ "Web stub with guidance to Stripe Elements"
7149
+ ],
7150
+ quickStart: `import { usePayments } from '@idealyst/payments';
7151
+
7152
+ const { isReady, isApplePayAvailable, confirmPayment } = usePayments({
7153
+ config: {
7154
+ publishableKey: 'pk_test_xxx',
7155
+ merchantIdentifier: 'merchant.com.myapp',
7156
+ merchantName: 'My App',
7157
+ merchantCountryCode: 'US',
7158
+ },
7159
+ });
7160
+
7161
+ const result = await confirmPayment({
7162
+ clientSecret: 'pi_xxx_secret_xxx',
7163
+ amount: { amount: 1099, currencyCode: 'usd' },
7164
+ });`,
7165
+ apiHighlights: [
7166
+ "initializePayments(config) - Initialize Stripe SDK",
7167
+ "checkPaymentAvailability() - Check Apple Pay / Google Pay / card",
7168
+ "confirmPayment(request) - Present sheet and confirm PaymentIntent",
7169
+ "createPaymentMethod(request) - Present sheet and create payment method",
7170
+ "getPaymentStatus() - Get current provider status",
7171
+ "usePayments(options?) - Convenience hook with state management"
7172
+ ],
7173
+ relatedPackages: ["biometrics", "oauth-client"]
6902
7174
  }
6903
7175
  };
6904
7176
  function getPackagesByCategory() {
@@ -9471,8 +9743,8 @@ var getStorageGuideDefinition = {
9471
9743
  properties: {
9472
9744
  topic: {
9473
9745
  type: "string",
9474
- description: "Topic to get docs for: 'overview', 'api', 'examples'",
9475
- enum: ["overview", "api", "examples"]
9746
+ description: "Topic to get docs for: 'overview', 'api', 'examples', 'secure'",
9747
+ enum: ["overview", "api", "examples", "secure"]
9476
9748
  }
9477
9749
  },
9478
9750
  required: ["topic"]
@@ -9643,6 +9915,51 @@ var getChartsGuideDefinition = {
9643
9915
  required: ["topic"]
9644
9916
  }
9645
9917
  };
9918
+ var getClipboardGuideDefinition = {
9919
+ name: "get_clipboard_guide",
9920
+ description: "Get documentation for @idealyst/clipboard cross-platform clipboard and OTP autofill package. Covers copy/paste API, useOTPAutoFill hook, and examples.",
9921
+ inputSchema: {
9922
+ type: "object",
9923
+ properties: {
9924
+ topic: {
9925
+ type: "string",
9926
+ description: "Topic to get docs for: 'overview', 'api', 'examples'",
9927
+ enum: ["overview", "api", "examples"]
9928
+ }
9929
+ },
9930
+ required: ["topic"]
9931
+ }
9932
+ };
9933
+ var getBiometricsGuideDefinition = {
9934
+ name: "get_biometrics_guide",
9935
+ description: "Get documentation for @idealyst/biometrics cross-platform biometric authentication and passkeys (WebAuthn/FIDO2) package. Covers local biometric auth, passkey registration/login, and examples.",
9936
+ inputSchema: {
9937
+ type: "object",
9938
+ properties: {
9939
+ topic: {
9940
+ type: "string",
9941
+ description: "Topic to get docs for: 'overview', 'api', 'examples'",
9942
+ enum: ["overview", "api", "examples"]
9943
+ }
9944
+ },
9945
+ required: ["topic"]
9946
+ }
9947
+ };
9948
+ var getPaymentsGuideDefinition = {
9949
+ name: "get_payments_guide",
9950
+ description: "Get documentation for @idealyst/payments cross-platform payment provider package. Covers Apple Pay, Google Pay, Stripe Platform Pay, usePayments hook, and examples.",
9951
+ inputSchema: {
9952
+ type: "object",
9953
+ properties: {
9954
+ topic: {
9955
+ type: "string",
9956
+ description: "Topic to get docs for: 'overview', 'api', 'examples'",
9957
+ enum: ["overview", "api", "examples"]
9958
+ }
9959
+ },
9960
+ required: ["topic"]
9961
+ }
9962
+ };
9646
9963
  var listPackagesDefinition = {
9647
9964
  name: "list_packages",
9648
9965
  description: "List all available Idealyst packages with descriptions, categories, and documentation status. Use this to discover what packages are available in the framework.",
@@ -9797,6 +10114,9 @@ var toolDefinitions = [
9797
10114
  getMarkdownGuideDefinition,
9798
10115
  getConfigGuideDefinition,
9799
10116
  getChartsGuideDefinition,
10117
+ getClipboardGuideDefinition,
10118
+ getBiometricsGuideDefinition,
10119
+ getPaymentsGuideDefinition,
9800
10120
  // Package tools
9801
10121
  listPackagesDefinition,
9802
10122
  getPackageDocsDefinition,
@@ -10589,7 +10909,8 @@ yarn add @idealyst/audio
10589
10909
  1. **PCM Streaming** \u2014 Audio data is delivered as \`PCMData\` chunks via callbacks, not as files
10590
10910
  2. **Audio Session** \u2014 On iOS/Android, configure the audio session category before recording/playback
10591
10911
  3. **Audio Profiles** \u2014 Pre-configured \`AudioConfig\` presets: \`speech\`, \`highQuality\`, \`studio\`, \`phone\`
10592
- 4. **Session Presets** \u2014 Pre-configured \`AudioSessionConfig\` presets: \`playback\`, \`record\`, \`voiceChat\`, \`ambient\`, \`default\`
10912
+ 4. **Session Presets** \u2014 Pre-configured \`AudioSessionConfig\` presets: \`playback\`, \`record\`, \`voiceChat\`, \`ambient\`, \`default\`, \`backgroundRecord\`
10913
+ 5. **Background Recording** \u2014 \`useBackgroundRecorder\` hook for recording that continues when the app is backgrounded (iOS/Android). Requires app-level native entitlements.
10593
10914
 
10594
10915
  ## Exports
10595
10916
 
@@ -10598,10 +10919,14 @@ import {
10598
10919
  useRecorder,
10599
10920
  usePlayer,
10600
10921
  useAudio,
10922
+ useBackgroundRecorder,
10601
10923
  AUDIO_PROFILES,
10602
10924
  SESSION_PRESETS,
10603
10925
  } from '@idealyst/audio';
10604
- import type { PCMData, AudioConfig, AudioLevel } from '@idealyst/audio';
10926
+ import type {
10927
+ PCMData, AudioConfig, AudioLevel,
10928
+ BackgroundRecorderStatus, BackgroundLifecycleInfo,
10929
+ } from '@idealyst/audio';
10605
10930
  \`\`\`
10606
10931
  `,
10607
10932
  "idealyst://audio/api": `# @idealyst/audio \u2014 API Reference
@@ -10711,6 +11036,84 @@ interface UseAudioOptions {
10711
11036
 
10712
11037
  ---
10713
11038
 
11039
+ ### useBackgroundRecorder(options?)
11040
+
11041
+ Background-aware recording hook. Wraps \`useRecorder\` with app lifecycle management for recording that continues when the app is backgrounded on iOS/Android. On web, works identically to \`useRecorder\` (background events never fire).
11042
+
11043
+ > **Requires app-level native configuration** \u2014 see "Background Recording Setup" below.
11044
+
11045
+ \`\`\`typescript
11046
+ interface UseBackgroundRecorderOptions {
11047
+ config?: Partial<AudioConfig>; // Audio config
11048
+ session?: Partial<AudioSessionConfig>; // Session config (default: SESSION_PRESETS.backgroundRecord)
11049
+ autoRequestPermission?: boolean; // Auto-request mic permission on mount
11050
+ levelUpdateInterval?: number; // Level update interval in ms (default: 100)
11051
+ maxBackgroundDuration?: number; // Max background recording time in ms (undefined = no limit)
11052
+ autoConfigureSession?: boolean; // Auto-configure session for background (default: true)
11053
+ onLifecycleEvent?: BackgroundLifecycleCallback; // Lifecycle event callback
11054
+ }
11055
+ \`\`\`
11056
+
11057
+ **Returns \`UseBackgroundRecorderResult\`:**
11058
+
11059
+ All properties from \`useRecorder\`, plus:
11060
+
11061
+ | Property | Type | Description |
11062
+ |----------|------|-------------|
11063
+ | isInBackground | boolean | Whether the app is currently backgrounded |
11064
+ | wasInterrupted | boolean | Whether recording was interrupted (phone call, Siri, etc.) |
11065
+ | backgroundDuration | number | Total time spent recording in background (ms) |
11066
+ | appState | AppStateStatus | Current app state (\`'active' \\| 'background' \\| 'inactive'\`) |
11067
+
11068
+ **Lifecycle events** (via \`onLifecycleEvent\`):
11069
+
11070
+ | Event | When | Extra fields |
11071
+ |-------|------|-------------|
11072
+ | \`'backgrounded'\` | App enters background while recording | \u2014 |
11073
+ | \`'foregrounded'\` | App returns to foreground while recording | \`backgroundDuration\` |
11074
+ | \`'interrupted'\` | OS interrupts recording (phone call, Siri) | \u2014 |
11075
+ | \`'interruptionEnded'\` | OS interruption ends | \`shouldResume\` |
11076
+ | \`'maxDurationReached'\` | Background recording hit \`maxBackgroundDuration\` | \`backgroundDuration\` |
11077
+ | \`'stopped'\` | Recording stopped while in background | \`backgroundDuration\` |
11078
+
11079
+ > **Note:** Interruptions use notify-only \u2014 the hook does NOT auto-resume. The consumer decides via the \`shouldResume\` flag.
11080
+
11081
+ #### Background Recording Setup
11082
+
11083
+ The OS will not allow background recording without app-level entitlements:
11084
+
11085
+ **iOS** \u2014 \`Info.plist\`:
11086
+ \`\`\`xml
11087
+ <key>UIBackgroundModes</key>
11088
+ <array><string>audio</string></array>
11089
+ \`\`\`
11090
+
11091
+ **Android** \u2014 \`AndroidManifest.xml\`:
11092
+ \`\`\`xml
11093
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
11094
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
11095
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
11096
+ <service
11097
+ android:name="com.swmansion.audioapi.system.CentralizedForegroundService"
11098
+ android:foregroundServiceType="microphone" />
11099
+ \`\`\`
11100
+
11101
+ **Expo** \u2014 \`app.json\` plugin:
11102
+ \`\`\`json
11103
+ ["react-native-audio-api", {
11104
+ "iosBackgroundMode": true,
11105
+ "androidForegroundService": true,
11106
+ "androidFSTypes": ["microphone"],
11107
+ "androidPermissions": [
11108
+ "android.permission.FOREGROUND_SERVICE",
11109
+ "android.permission.FOREGROUND_SERVICE_MICROPHONE",
11110
+ "android.permission.RECORD_AUDIO"
11111
+ ]
11112
+ }]
11113
+ \`\`\`
11114
+
11115
+ ---
11116
+
10714
11117
  ## Types
10715
11118
 
10716
11119
  ### AudioConfig
@@ -10772,6 +11175,7 @@ type AudioErrorCode =
10772
11175
  | 'DEVICE_NOT_FOUND' | 'DEVICE_IN_USE' | 'NOT_SUPPORTED'
10773
11176
  | 'SOURCE_NOT_FOUND' | 'FORMAT_NOT_SUPPORTED' | 'DECODE_ERROR' | 'PLAYBACK_ERROR' | 'BUFFER_UNDERRUN'
10774
11177
  | 'RECORDING_ERROR'
11178
+ | 'BACKGROUND_NOT_SUPPORTED' | 'BACKGROUND_MAX_DURATION'
10775
11179
  | 'INITIALIZATION_FAILED' | 'INVALID_STATE' | 'INVALID_CONFIG' | 'UNKNOWN';
10776
11180
 
10777
11181
  interface AudioError {
@@ -10800,11 +11204,12 @@ const AUDIO_PROFILES: AudioProfiles = {
10800
11204
 
10801
11205
  \`\`\`typescript
10802
11206
  const SESSION_PRESETS: SessionPresets = {
10803
- playback: { category: 'playback', mode: 'default' },
10804
- record: { category: 'record', mode: 'default' },
10805
- voiceChat: { category: 'playAndRecord', mode: 'voiceChat', categoryOptions: ['allowBluetooth', 'defaultToSpeaker'] },
10806
- ambient: { category: 'ambient', mode: 'default' },
10807
- default: { category: 'soloAmbient', mode: 'default' },
11207
+ playback: { category: 'playback', mode: 'default' },
11208
+ record: { category: 'record', mode: 'default' },
11209
+ voiceChat: { category: 'playAndRecord', mode: 'voiceChat', categoryOptions: ['allowBluetooth', 'defaultToSpeaker'] },
11210
+ ambient: { category: 'ambient', mode: 'default' },
11211
+ default: { category: 'soloAmbient', mode: 'default' },
11212
+ backgroundRecord: { category: 'playAndRecord', mode: 'spokenAudio', categoryOptions: ['defaultToSpeaker', 'allowBluetooth', 'allowBluetoothA2DP', 'mixWithOthers'] },
10808
11213
  };
10809
11214
  \`\`\`
10810
11215
  `,
@@ -10977,6 +11382,77 @@ function VoiceChatScreen() {
10977
11382
  }
10978
11383
  \`\`\`
10979
11384
 
11385
+ ## Background Recording for Transcription
11386
+
11387
+ \`\`\`tsx
11388
+ import React, { useEffect } from 'react';
11389
+ import { View, Button, Text } from '@idealyst/components';
11390
+ import { useBackgroundRecorder, AUDIO_PROFILES } from '@idealyst/audio';
11391
+ import type { PCMData, BackgroundLifecycleInfo } from '@idealyst/audio';
11392
+
11393
+ function BackgroundTranscriber() {
11394
+ const recorder = useBackgroundRecorder({
11395
+ config: AUDIO_PROFILES.speech,
11396
+ maxBackgroundDuration: 5 * 60 * 1000, // 5 min max in background
11397
+ onLifecycleEvent: (info: BackgroundLifecycleInfo) => {
11398
+ switch (info.event) {
11399
+ case 'backgrounded':
11400
+ console.log('Recording continues in background');
11401
+ break;
11402
+ case 'foregrounded':
11403
+ console.log(\`Back from background after \${info.backgroundDuration}ms\`);
11404
+ break;
11405
+ case 'interrupted':
11406
+ console.log('Recording interrupted (phone call?)');
11407
+ break;
11408
+ case 'interruptionEnded':
11409
+ if (info.shouldResume) {
11410
+ recorder.resume(); // Consumer decides whether to resume
11411
+ }
11412
+ break;
11413
+ case 'maxDurationReached':
11414
+ console.log('Max background duration reached');
11415
+ break;
11416
+ }
11417
+ },
11418
+ });
11419
+
11420
+ // Stream PCM chunks to your speech-to-text service
11421
+ useEffect(() => {
11422
+ const unsub = recorder.subscribeToData((pcm: PCMData) => {
11423
+ // Send to STT API (e.g., Whisper, Deepgram)
11424
+ sendToTranscriptionService(pcm.toBase64());
11425
+ });
11426
+ return unsub;
11427
+ }, [recorder.subscribeToData]);
11428
+
11429
+ const handleToggle = async () => {
11430
+ if (recorder.isRecording) {
11431
+ await recorder.stop();
11432
+ } else {
11433
+ await recorder.start();
11434
+ }
11435
+ };
11436
+
11437
+ return (
11438
+ <View padding="md" gap="md">
11439
+ <Button
11440
+ onPress={handleToggle}
11441
+ intent={recorder.isRecording ? 'error' : 'primary'}
11442
+ >
11443
+ {recorder.isRecording ? 'Stop' : 'Record'}
11444
+ </Button>
11445
+ <Text>Duration: {Math.round(recorder.duration / 1000)}s</Text>
11446
+ {recorder.isInBackground && <Text>Recording in background...</Text>}
11447
+ {recorder.wasInterrupted && <Text>Recording was interrupted</Text>}
11448
+ <Text>Background time: {Math.round(recorder.backgroundDuration / 1000)}s</Text>
11449
+ </View>
11450
+ );
11451
+ }
11452
+ \`\`\`
11453
+
11454
+ > **Important:** Background recording requires native entitlements. See the \`useBackgroundRecorder\` API docs for iOS, Android, and Expo setup instructions.
11455
+
10980
11456
  ## Audio Level Visualization
10981
11457
 
10982
11458
  \`\`\`tsx
@@ -14424,192 +14900,1708 @@ type CurveType =
14424
14900
  > - \`tickFormat\` signature is \`(value: number | string | Date) => string\` \u2014 NOT \`(v: number) => string\`.
14425
14901
  > - \`AxisConfig\` uses \`show\` (NOT \`visible\`) to control axis visibility.
14426
14902
 
14427
- ## Line Chart
14903
+ ## Line Chart
14904
+
14905
+ \`\`\`tsx
14906
+ import React from 'react';
14907
+ import { View, Text } from '@idealyst/components';
14908
+ import { LineChart } from '@idealyst/charts';
14909
+ import type { ChartDataSeries } from '@idealyst/charts';
14910
+
14911
+ const monthlySales = [
14912
+ { x: 'Jan', y: 4200 },
14913
+ { x: 'Feb', y: 5100 },
14914
+ { x: 'Mar', y: 4800 },
14915
+ { x: 'Apr', y: 6200 },
14916
+ { x: 'May', y: 5900 },
14917
+ { x: 'Jun', y: 7100 },
14918
+ ];
14919
+
14920
+ // Series uses 'name' for display \u2014 NOT 'label'
14921
+ const revenueData: ChartDataSeries[] = [
14922
+ { id: 'revenue', name: 'Revenue', data: monthlySales, color: '#4CAF50' },
14923
+ ];
14924
+
14925
+ function SalesOverview() {
14926
+ return (
14927
+ <View padding="md" gap="md">
14928
+ <Text typography="h6" weight="bold">Monthly Sales</Text>
14929
+ <LineChart
14930
+ data={revenueData}
14931
+ height={300}
14932
+ curve="monotone"
14933
+ showDots
14934
+ showArea
14935
+ areaOpacity={0.15}
14936
+ animate
14937
+ xAxis={{ label: 'Month' }}
14938
+ yAxis={{
14939
+ label: 'Revenue ($)',
14940
+ // tickFormat param type is (value: number | string | Date) => string
14941
+ tickFormat: (value: number | string | Date) => \`$\${Number(value) / 1000}k\`,
14942
+ }}
14943
+ />
14944
+ </View>
14945
+ );
14946
+ }
14947
+ \`\`\`
14948
+
14949
+ ## Multi-Series Line Chart
14950
+
14951
+ \`\`\`tsx
14952
+ import React from 'react';
14953
+ import { LineChart } from '@idealyst/charts';
14954
+ import type { ChartDataSeries } from '@idealyst/charts';
14955
+
14956
+ // Each series has: id, name, data, color? \u2014 NO 'label' property
14957
+ const series: ChartDataSeries[] = [
14958
+ {
14959
+ id: 'product-a',
14960
+ name: 'Product A', // Use 'name' \u2014 NOT 'label'
14961
+ data: [
14962
+ { x: 'Q1', y: 120 },
14963
+ { x: 'Q2', y: 150 },
14964
+ { x: 'Q3', y: 180 },
14965
+ { x: 'Q4', y: 210 },
14966
+ ],
14967
+ color: '#2196F3',
14968
+ },
14969
+ {
14970
+ id: 'product-b',
14971
+ name: 'Product B', // Use 'name' \u2014 NOT 'label'
14972
+ data: [
14973
+ { x: 'Q1', y: 80 },
14974
+ { x: 'Q2', y: 110 },
14975
+ { x: 'Q3', y: 95 },
14976
+ { x: 'Q4', y: 140 },
14977
+ ],
14978
+ color: '#FF9800',
14979
+ },
14980
+ ];
14981
+
14982
+ function ComparisonChart() {
14983
+ return (
14984
+ <LineChart
14985
+ data={series}
14986
+ height={350}
14987
+ curve="monotone"
14988
+ showDots
14989
+ animate
14990
+ />
14991
+ );
14992
+ }
14993
+ \`\`\`
14994
+
14995
+ ## Bar Chart
14996
+
14997
+ \`\`\`tsx
14998
+ import React from 'react';
14999
+ import { View, Text } from '@idealyst/components';
15000
+ import { BarChart } from '@idealyst/charts';
15001
+
15002
+ const categories = [
15003
+ { x: 'Electronics', y: 45 },
15004
+ { x: 'Clothing', y: 32 },
15005
+ { x: 'Books', y: 18 },
15006
+ { x: 'Food', y: 56 },
15007
+ { x: 'Sports', y: 28 },
15008
+ ];
15009
+
15010
+ function CategoryBreakdown() {
15011
+ return (
15012
+ <View padding="md" gap="md">
15013
+ <Text typography="h6" weight="bold">Sales by Category</Text>
15014
+ <BarChart
15015
+ data={[{ id: 'units', name: 'Units Sold', data: categories }]}
15016
+ height={300}
15017
+ barRadius={4}
15018
+ animate
15019
+ yAxis={{ tickFormat: (value: number | string | Date) => \`\${value} units\` }}
15020
+ />
15021
+ </View>
15022
+ );
15023
+ }
15024
+ \`\`\`
15025
+
15026
+ ## Stacked Bar Chart
15027
+
15028
+ \`\`\`tsx
15029
+ import React from 'react';
15030
+ import { BarChart } from '@idealyst/charts';
15031
+
15032
+ function StackedBarExample() {
15033
+ return (
15034
+ <BarChart
15035
+ data={[
15036
+ {
15037
+ id: 'online',
15038
+ name: 'Online',
15039
+ data: [
15040
+ { x: 'Q1', y: 100 },
15041
+ { x: 'Q2', y: 120 },
15042
+ { x: 'Q3', y: 90 },
15043
+ ],
15044
+ color: '#4CAF50',
15045
+ },
15046
+ {
15047
+ id: 'in-store',
15048
+ name: 'In-Store',
15049
+ data: [
15050
+ { x: 'Q1', y: 60 },
15051
+ { x: 'Q2', y: 80 },
15052
+ { x: 'Q3', y: 70 },
15053
+ ],
15054
+ color: '#2196F3',
15055
+ },
15056
+ ]}
15057
+ height={300}
15058
+ stacked
15059
+ animate
15060
+ />
15061
+ );
15062
+ }
15063
+ \`\`\`
15064
+
15065
+ ## Horizontal Bar Chart
15066
+
15067
+ \`\`\`tsx
15068
+ import React from 'react';
15069
+ import { BarChart } from '@idealyst/charts';
15070
+
15071
+ function HorizontalBarExample() {
15072
+ const data = [
15073
+ { x: 'React', y: 85 },
15074
+ { x: 'Vue', y: 62 },
15075
+ { x: 'Angular', y: 45 },
15076
+ { x: 'Svelte', y: 38 },
15077
+ ];
15078
+
15079
+ return (
15080
+ <BarChart
15081
+ data={[{ id: 'popularity', name: 'Popularity', data }]}
15082
+ height={250}
15083
+ orientation="horizontal"
15084
+ animate
15085
+ />
15086
+ );
15087
+ }
15088
+ \`\`\`
15089
+ `
15090
+ };
15091
+
15092
+ // src/data/clipboard-guides.ts
15093
+ var clipboardGuides = {
15094
+ "idealyst://clipboard/overview": `# @idealyst/clipboard Overview
15095
+
15096
+ Cross-platform clipboard and OTP autofill for React and React Native applications. Provides a consistent async API for copy/paste operations, plus a mobile-only hook for automatic SMS OTP code detection.
15097
+
15098
+ ## Features
15099
+
15100
+ - **Cross-Platform Clipboard** - Copy and paste text on React Native and Web
15101
+ - **Simple API** - Async/await based with consistent interface
15102
+ - **React Native** - Uses @react-native-clipboard/clipboard
15103
+ - **Web** - Uses navigator.clipboard API
15104
+ - **OTP Auto-Fill (Android)** - Automatically reads OTP codes from SMS via SMS Retriever API (no permissions needed)
15105
+ - **OTP Auto-Fill (iOS)** - Provides TextInput props for native iOS keyboard OTP suggestion
15106
+ - **TypeScript** - Full type safety and IntelliSense support
15107
+
15108
+ ## Installation
15109
+
15110
+ \`\`\`bash
15111
+ yarn add @idealyst/clipboard
15112
+
15113
+ # React Native also needs:
15114
+ yarn add @react-native-clipboard/clipboard
15115
+ cd ios && pod install
15116
+
15117
+ # For OTP autofill on Android (optional):
15118
+ yarn add react-native-otp-verify
15119
+ \`\`\`
15120
+
15121
+ ## Quick Start
15122
+
15123
+ \`\`\`tsx
15124
+ import { clipboard } from '@idealyst/clipboard';
15125
+
15126
+ // Copy text
15127
+ await clipboard.copy('Hello, world!');
15128
+
15129
+ // Paste text
15130
+ const text = await clipboard.paste();
15131
+
15132
+ // Check if clipboard has text
15133
+ const hasText = await clipboard.hasText();
15134
+ \`\`\`
15135
+
15136
+ ## OTP Auto-Fill Quick Start
15137
+
15138
+ \`\`\`tsx
15139
+ import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
15140
+ import { TextInput } from 'react-native';
15141
+
15142
+ function OTPScreen() {
15143
+ const { code, startListening, hash } = useOTPAutoFill({
15144
+ codeLength: 6,
15145
+ onCodeReceived: (otp) => verifyOTP(otp),
15146
+ });
15147
+
15148
+ useEffect(() => {
15149
+ startListening();
15150
+ }, []);
15151
+
15152
+ return (
15153
+ <TextInput
15154
+ value={code ?? ''}
15155
+ {...OTP_INPUT_PROPS}
15156
+ />
15157
+ );
15158
+ }
15159
+ \`\`\`
15160
+
15161
+ ## Import Options
15162
+
15163
+ \`\`\`tsx
15164
+ // Named import (recommended)
15165
+ import { clipboard } from '@idealyst/clipboard';
15166
+
15167
+ // Default import
15168
+ import clipboard from '@idealyst/clipboard';
15169
+
15170
+ // OTP hook and helpers
15171
+ import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
15172
+ \`\`\`
15173
+
15174
+ ## Platform Details
15175
+
15176
+ - **React Native**: Uses \`@react-native-clipboard/clipboard\` for clipboard operations
15177
+ - **Web**: Uses \`navigator.clipboard\` API (requires secure context / HTTPS)
15178
+ - **OTP (Android)**: Uses SMS Retriever API via \`react-native-otp-verify\` \u2014 zero permissions
15179
+ - **OTP (iOS)**: Native keyboard autofill via \`textContentType="oneTimeCode"\`
15180
+ - **OTP (Web)**: No-op \u2014 returns null values and noop functions
15181
+ `,
15182
+ "idealyst://clipboard/api": `# Clipboard API Reference
15183
+
15184
+ Complete API reference for @idealyst/clipboard.
15185
+
15186
+ ## clipboard.copy
15187
+
15188
+ Copy text to the system clipboard.
15189
+
15190
+ \`\`\`tsx
15191
+ await clipboard.copy(text: string): Promise<void>
15192
+
15193
+ // Examples
15194
+ await clipboard.copy('Hello, world!');
15195
+ await clipboard.copy(inviteCode);
15196
+ await clipboard.copy(JSON.stringify(data));
15197
+ \`\`\`
15198
+
15199
+ ## clipboard.paste
15200
+
15201
+ Read text from the system clipboard.
15202
+
15203
+ \`\`\`tsx
15204
+ await clipboard.paste(): Promise<string>
15205
+
15206
+ // Examples
15207
+ const text = await clipboard.paste();
15208
+ const url = await clipboard.paste();
15209
+ \`\`\`
15210
+
15211
+ ## clipboard.hasText
15212
+
15213
+ Check if the clipboard contains text content.
15214
+
15215
+ \`\`\`tsx
15216
+ await clipboard.hasText(): Promise<boolean>
15217
+
15218
+ // Example
15219
+ const canPaste = await clipboard.hasText();
15220
+ if (canPaste) {
15221
+ const text = await clipboard.paste();
15222
+ }
15223
+ \`\`\`
15224
+
15225
+ ## clipboard.addListener
15226
+
15227
+ Listen for copy events (triggered when \`clipboard.copy()\` is called).
15228
+
15229
+ \`\`\`tsx
15230
+ const unsubscribe = clipboard.addListener((content: string) => {
15231
+ console.log('Copied:', content);
15232
+ });
15233
+
15234
+ // Later, unsubscribe
15235
+ unsubscribe();
15236
+ \`\`\`
15237
+
15238
+ ---
15239
+
15240
+ ## useOTPAutoFill
15241
+
15242
+ React hook for automatic OTP code detection from SMS on mobile.
15243
+
15244
+ **Android**: Uses SMS Retriever API to auto-read OTP from incoming SMS (no permissions required). SMS must include your app hash.
15245
+
15246
+ **iOS**: OTP is handled natively by the iOS keyboard. Use \`OTP_INPUT_PROPS\` on your TextInput to enable it.
15247
+
15248
+ **Web**: Returns no-op values.
15249
+
15250
+ \`\`\`tsx
15251
+ const {
15252
+ code, // string | null - received OTP code (Android only)
15253
+ startListening, // () => void - begin listening for SMS (Android only)
15254
+ stopListening, // () => void - stop listening (Android only)
15255
+ hash, // string | null - app hash for SMS body (Android only)
15256
+ } = useOTPAutoFill(options?: {
15257
+ codeLength?: number; // default: 6
15258
+ onCodeReceived?: (code: string) => void;
15259
+ });
15260
+ \`\`\`
15261
+
15262
+ ### Options
15263
+
15264
+ | Option | Type | Default | Description |
15265
+ |--------|------|---------|-------------|
15266
+ | codeLength | number | 6 | Expected digit count of OTP code |
15267
+ | onCodeReceived | (code: string) => void | \u2014 | Callback when OTP is detected |
15268
+
15269
+ ### Return Value
15270
+
15271
+ | Property | Type | Description |
15272
+ |----------|------|-------------|
15273
+ | code | string \\| null | Detected OTP code (Android). Null on iOS/web. |
15274
+ | startListening | () => void | Start SMS listener (Android). No-op on iOS/web. |
15275
+ | stopListening | () => void | Stop SMS listener (Android). No-op on iOS/web. |
15276
+ | hash | string \\| null | App hash for SMS Retriever (Android). Null on iOS/web. |
15277
+
15278
+ ### Android SMS Format
15279
+
15280
+ For the SMS Retriever API to detect the message, the SMS must:
15281
+ 1. Start with \`<#>\`
15282
+ 2. Contain the OTP code as a sequence of digits
15283
+ 3. End with your app's 11-character hash (available via \`hash\`)
15284
+
15285
+ Example SMS:
15286
+ \`\`\`
15287
+ <#> Your verification code is: 123456
15288
+ FA+9qCX9VSu
15289
+ \`\`\`
15290
+
15291
+ ---
15292
+
15293
+ ## OTP_INPUT_PROPS
15294
+
15295
+ Constant with TextInput props to enable native OTP keyboard autofill.
15296
+
15297
+ \`\`\`tsx
15298
+ import { OTP_INPUT_PROPS } from '@idealyst/clipboard';
15299
+
15300
+ // Value:
15301
+ // {
15302
+ // textContentType: 'oneTimeCode',
15303
+ // autoComplete: 'sms-otp',
15304
+ // }
15305
+
15306
+ <TextInput
15307
+ {...OTP_INPUT_PROPS}
15308
+ value={code}
15309
+ onChangeText={setCode}
15310
+ />
15311
+ \`\`\`
15312
+
15313
+ - **iOS**: Enables the keyboard to suggest OTP codes from received SMS (iOS 12+)
15314
+ - **Android**: Maps to the correct \`autoComplete\` value
15315
+ - **Web**: Harmless \u2014 ignored by web TextInput
15316
+ `,
15317
+ "idealyst://clipboard/examples": `# Clipboard Examples
15318
+
15319
+ Complete code examples for common @idealyst/clipboard patterns.
15320
+
15321
+ ## Copy to Clipboard with Feedback
15322
+
15323
+ \`\`\`tsx
15324
+ import { clipboard } from '@idealyst/clipboard';
15325
+ import { useState, useCallback } from 'react';
15326
+
15327
+ function CopyButton({ text }: { text: string }) {
15328
+ const [copied, setCopied] = useState(false);
15329
+
15330
+ const handleCopy = useCallback(async () => {
15331
+ await clipboard.copy(text);
15332
+ setCopied(true);
15333
+ setTimeout(() => setCopied(false), 2000);
15334
+ }, [text]);
15335
+
15336
+ return (
15337
+ <Button
15338
+ label={copied ? 'Copied!' : 'Copy'}
15339
+ intent={copied ? 'positive' : 'neutral'}
15340
+ onPress={handleCopy}
15341
+ />
15342
+ );
15343
+ }
15344
+ \`\`\`
15345
+
15346
+ ## Share / Copy Link
15347
+
15348
+ \`\`\`tsx
15349
+ import { clipboard } from '@idealyst/clipboard';
15350
+
15351
+ async function copyShareLink(itemId: string) {
15352
+ const url = \`https://myapp.com/items/\${itemId}\`;
15353
+ await clipboard.copy(url);
15354
+ }
15355
+ \`\`\`
15356
+
15357
+ ## Paste from Clipboard
15358
+
15359
+ \`\`\`tsx
15360
+ import { clipboard } from '@idealyst/clipboard';
15361
+
15362
+ function PasteInput() {
15363
+ const [value, setValue] = useState('');
15364
+
15365
+ const handlePaste = useCallback(async () => {
15366
+ const hasText = await clipboard.hasText();
15367
+ if (hasText) {
15368
+ const text = await clipboard.paste();
15369
+ setValue(text);
15370
+ }
15371
+ }, []);
15372
+
15373
+ return (
15374
+ <View>
15375
+ <Input value={value} onChangeText={setValue} placeholder="Enter or paste text" />
15376
+ <Button label="Paste" onPress={handlePaste} iconName="content-paste" />
15377
+ </View>
15378
+ );
15379
+ }
15380
+ \`\`\`
15381
+
15382
+ ## useClipboard Hook
15383
+
15384
+ \`\`\`tsx
15385
+ import { clipboard } from '@idealyst/clipboard';
15386
+ import { useState, useCallback } from 'react';
15387
+
15388
+ export function useClipboard() {
15389
+ const [copiedText, setCopiedText] = useState<string | null>(null);
15390
+
15391
+ const copy = useCallback(async (text: string) => {
15392
+ await clipboard.copy(text);
15393
+ setCopiedText(text);
15394
+ }, []);
15395
+
15396
+ const paste = useCallback(async () => {
15397
+ return clipboard.paste();
15398
+ }, []);
15399
+
15400
+ const reset = useCallback(() => {
15401
+ setCopiedText(null);
15402
+ }, []);
15403
+
15404
+ return { copy, paste, copiedText, reset };
15405
+ }
15406
+ \`\`\`
15407
+
15408
+ ## OTP Verification Screen
15409
+
15410
+ \`\`\`tsx
15411
+ import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
15412
+ import { useEffect, useState } from 'react';
15413
+ import { TextInput, Platform } from 'react-native';
15414
+
15415
+ function OTPVerificationScreen({ phoneNumber, onVerify }: {
15416
+ phoneNumber: string;
15417
+ onVerify: (code: string) => void;
15418
+ }) {
15419
+ const [code, setCode] = useState('');
15420
+
15421
+ const otp = useOTPAutoFill({
15422
+ codeLength: 6,
15423
+ onCodeReceived: (receivedCode) => {
15424
+ setCode(receivedCode);
15425
+ onVerify(receivedCode);
15426
+ },
15427
+ });
15428
+
15429
+ // Start listening when screen mounts
15430
+ useEffect(() => {
15431
+ otp.startListening();
15432
+ return () => otp.stopListening();
15433
+ }, []);
15434
+
15435
+ // Auto-fill from hook on Android
15436
+ useEffect(() => {
15437
+ if (otp.code) {
15438
+ setCode(otp.code);
15439
+ }
15440
+ }, [otp.code]);
15441
+
15442
+ return (
15443
+ <View>
15444
+ <Text>Enter the code sent to {phoneNumber}</Text>
15445
+
15446
+ <TextInput
15447
+ value={code}
15448
+ onChangeText={(text) => {
15449
+ setCode(text);
15450
+ if (text.length === 6) onVerify(text);
15451
+ }}
15452
+ keyboardType="number-pad"
15453
+ maxLength={6}
15454
+ {...OTP_INPUT_PROPS}
15455
+ />
15456
+
15457
+ {Platform.OS === 'android' && otp.hash && (
15458
+ <Text style={{ fontSize: 10, color: '#999' }}>
15459
+ App hash (for SMS setup): {otp.hash}
15460
+ </Text>
15461
+ )}
15462
+ </View>
15463
+ );
15464
+ }
15465
+ \`\`\`
15466
+
15467
+ ## Copy Invite Code
15468
+
15469
+ \`\`\`tsx
15470
+ import { clipboard } from '@idealyst/clipboard';
15471
+
15472
+ function InviteCard({ code }: { code: string }) {
15473
+ const [copied, setCopied] = useState(false);
15474
+
15475
+ return (
15476
+ <Card>
15477
+ <Text>Your invite code</Text>
15478
+ <Text variant="headline">{code}</Text>
15479
+ <Button
15480
+ label={copied ? 'Copied!' : 'Copy Code'}
15481
+ iconName={copied ? 'check' : 'content-copy'}
15482
+ onPress={async () => {
15483
+ await clipboard.copy(code);
15484
+ setCopied(true);
15485
+ setTimeout(() => setCopied(false), 2000);
15486
+ }}
15487
+ />
15488
+ </Card>
15489
+ );
15490
+ }
15491
+ \`\`\`
15492
+
15493
+ ## Error Handling
15494
+
15495
+ \`\`\`tsx
15496
+ import { clipboard } from '@idealyst/clipboard';
15497
+
15498
+ async function safeCopy(text: string): Promise<boolean> {
15499
+ try {
15500
+ await clipboard.copy(text);
15501
+ return true;
15502
+ } catch (error) {
15503
+ console.error('Clipboard copy failed:', error);
15504
+ // On web, this can fail if not in a secure context or without user gesture
15505
+ return false;
15506
+ }
15507
+ }
15508
+
15509
+ async function safePaste(): Promise<string | null> {
15510
+ try {
15511
+ const hasText = await clipboard.hasText();
15512
+ if (!hasText) return null;
15513
+ return await clipboard.paste();
15514
+ } catch (error) {
15515
+ console.error('Clipboard paste failed:', error);
15516
+ // Browser may deny permission
15517
+ return null;
15518
+ }
15519
+ }
15520
+ \`\`\`
15521
+
15522
+ ## Best Practices
15523
+
15524
+ 1. **Always use try-catch** \u2014 Clipboard operations can fail (permissions, secure context)
15525
+ 2. **Provide visual feedback** \u2014 Show a "Copied!" confirmation after copying
15526
+ 3. **Use OTP_INPUT_PROPS** \u2014 Always spread these on OTP text inputs for cross-platform autofill
15527
+ 4. **Start OTP listener on mount** \u2014 Call \`startListening()\` in useEffect, clean up with \`stopListening()\`
15528
+ 5. **Log the Android hash** \u2014 During development, log \`hash\` to configure your SMS gateway
15529
+ 6. **Graceful degradation** \u2014 OTP features degrade gracefully if native modules aren't installed
15530
+ 7. **Secure context (web)** \u2014 Clipboard API requires HTTPS on web
15531
+ `
15532
+ };
15533
+
15534
+ // src/data/biometrics-guides.ts
15535
+ var biometricsGuides = {
15536
+ "idealyst://biometrics/overview": `# @idealyst/biometrics Overview
15537
+
15538
+ Cross-platform biometric authentication and passkeys (WebAuthn/FIDO2) for React and React Native applications.
15539
+
15540
+ ## Features
15541
+
15542
+ - **Local Biometric Auth** \u2014 FaceID, TouchID, fingerprint, iris to gate access
15543
+ - **Passkeys (WebAuthn/FIDO2)** \u2014 Passwordless login with cryptographic credentials
15544
+ - **Cross-Platform** \u2014 Single API for React Native (iOS/Android) and Web
15545
+ - **React Native** \u2014 Uses expo-local-authentication for biometrics, react-native-passkeys for passkeys
15546
+ - **Web** \u2014 Uses WebAuthn API for both biometrics and passkeys
15547
+ - **Graceful Degradation** \u2014 Falls back cleanly when native modules aren't installed
15548
+ - **TypeScript** \u2014 Full type safety and IntelliSense support
15549
+
15550
+ ## Installation
15551
+
15552
+ \`\`\`bash
15553
+ yarn add @idealyst/biometrics
15554
+
15555
+ # React Native \u2014 biometric auth:
15556
+ yarn add expo-local-authentication
15557
+ cd ios && pod install
15558
+
15559
+ # React Native \u2014 passkeys (optional):
15560
+ yarn add react-native-passkeys
15561
+ cd ios && pod install
15562
+ \`\`\`
15563
+
15564
+ ## Quick Start \u2014 Biometric Auth
15565
+
15566
+ \`\`\`tsx
15567
+ import { isBiometricAvailable, authenticate } from '@idealyst/biometrics';
15568
+
15569
+ // Check availability
15570
+ const available = await isBiometricAvailable();
15571
+
15572
+ // Prompt user
15573
+ if (available) {
15574
+ const result = await authenticate({
15575
+ promptMessage: 'Verify your identity',
15576
+ });
15577
+
15578
+ if (result.success) {
15579
+ // Authenticated!
15580
+ } else {
15581
+ console.log(result.error, result.message);
15582
+ }
15583
+ }
15584
+ \`\`\`
15585
+
15586
+ ## Quick Start \u2014 Passkeys
15587
+
15588
+ \`\`\`tsx
15589
+ import { isPasskeySupported, createPasskey, getPasskey } from '@idealyst/biometrics';
15590
+
15591
+ // Check support
15592
+ const supported = await isPasskeySupported();
15593
+
15594
+ // Register a new passkey
15595
+ const credential = await createPasskey({
15596
+ challenge: serverChallenge,
15597
+ rp: { id: 'example.com', name: 'My App' },
15598
+ user: { id: userId, name: email, displayName: name },
15599
+ });
15600
+ // Send credential to server for verification
15601
+
15602
+ // Sign in with passkey
15603
+ const assertion = await getPasskey({
15604
+ challenge: serverChallenge,
15605
+ rpId: 'example.com',
15606
+ });
15607
+ // Send assertion to server for verification
15608
+ \`\`\`
15609
+
15610
+ ## Platform Details
15611
+
15612
+ - **React Native (biometrics)**: Uses \`expo-local-authentication\` \u2014 FaceID, TouchID, fingerprint, iris
15613
+ - **React Native (passkeys)**: Uses \`react-native-passkeys\` \u2014 system passkey UI on iOS 16+ and Android 9+
15614
+ - **Web (biometrics)**: Uses WebAuthn with \`userVerification: 'required'\` to trigger platform authenticator
15615
+ - **Web (passkeys)**: Uses \`navigator.credentials.create/get\` with the PublicKey API
15616
+ `,
15617
+ "idealyst://biometrics/api": `# Biometrics API Reference
15618
+
15619
+ Complete API reference for @idealyst/biometrics.
15620
+
15621
+ ---
15622
+
15623
+ ## Biometric Authentication Functions
15624
+
15625
+ ### isBiometricAvailable
15626
+
15627
+ Check whether biometric auth hardware is available and enrolled.
15628
+
15629
+ \`\`\`tsx
15630
+ await isBiometricAvailable(): Promise<boolean>
15631
+
15632
+ // Native: checks hardware + enrollment via expo-local-authentication
15633
+ // Web: checks PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
15634
+ \`\`\`
15635
+
15636
+ ### getBiometricTypes
15637
+
15638
+ Return the biometric types available on this device.
15639
+
15640
+ \`\`\`tsx
15641
+ await getBiometricTypes(): Promise<BiometricType[]>
15642
+
15643
+ type BiometricType = 'fingerprint' | 'facial_recognition' | 'iris';
15644
+
15645
+ // Native: returns specific types (fingerprint, facial_recognition, iris)
15646
+ // Web: returns ['fingerprint'] as generic indicator, or [] if unavailable
15647
+ \`\`\`
15648
+
15649
+ ### getSecurityLevel
15650
+
15651
+ Get the security level of biometric authentication on the device.
15652
+
15653
+ \`\`\`tsx
15654
+ await getSecurityLevel(): Promise<SecurityLevel>
15655
+
15656
+ type SecurityLevel = 'none' | 'device_credential' | 'biometric_weak' | 'biometric_strong';
15657
+
15658
+ // Native: maps expo-local-authentication SecurityLevel enum
15659
+ // Web: returns 'biometric_strong' if platform authenticator available, else 'none'
15660
+ \`\`\`
15661
+
15662
+ ### authenticate
15663
+
15664
+ Prompt the user for biometric authentication.
15665
+
15666
+ \`\`\`tsx
15667
+ await authenticate(options?: AuthenticateOptions): Promise<AuthResult>
15668
+ \`\`\`
15669
+
15670
+ **AuthenticateOptions:**
15671
+
15672
+ | Option | Type | Default | Description |
15673
+ |--------|------|---------|-------------|
15674
+ | promptMessage | string | 'Authenticate' | Message shown alongside the biometric prompt |
15675
+ | cancelLabel | string | \u2014 | Label for the cancel button |
15676
+ | fallbackLabel | string | \u2014 | iOS: label for passcode fallback button |
15677
+ | disableDeviceFallback | boolean | false | Prevent PIN/passcode fallback after biometric failure |
15678
+ | requireStrongBiometric | boolean | false | Android: require Class 3 (strong) biometric |
15679
+
15680
+ **AuthResult:**
15681
+
15682
+ \`\`\`tsx
15683
+ type AuthResult =
15684
+ | { success: true }
15685
+ | { success: false; error: AuthError; message?: string };
15686
+
15687
+ type AuthError =
15688
+ | 'not_available'
15689
+ | 'not_enrolled'
15690
+ | 'user_cancel'
15691
+ | 'lockout'
15692
+ | 'system_cancel'
15693
+ | 'passcode_not_set'
15694
+ | 'authentication_failed'
15695
+ | 'unknown';
15696
+ \`\`\`
15697
+
15698
+ ### cancelAuthentication
15699
+
15700
+ Cancel an in-progress authentication prompt (Android only). No-op on iOS and web.
15701
+
15702
+ \`\`\`tsx
15703
+ await cancelAuthentication(): Promise<void>
15704
+ \`\`\`
15705
+
15706
+ ---
15707
+
15708
+ ## Passkey Functions
15709
+
15710
+ ### isPasskeySupported
15711
+
15712
+ Check if passkeys (WebAuthn/FIDO2) are supported on this device/browser.
15713
+
15714
+ \`\`\`tsx
15715
+ await isPasskeySupported(): Promise<boolean>
15716
+
15717
+ // Web: checks PublicKeyCredential + isUserVerifyingPlatformAuthenticatorAvailable
15718
+ // Native: checks react-native-passkeys Passkey.isSupported()
15719
+ \`\`\`
15720
+
15721
+ ### createPasskey
15722
+
15723
+ Create a new passkey credential (registration / attestation ceremony).
15724
+
15725
+ \`\`\`tsx
15726
+ await createPasskey(options: PasskeyCreateOptions): Promise<PasskeyCreateResult>
15727
+ \`\`\`
15728
+
15729
+ **PasskeyCreateOptions:**
15730
+
15731
+ | Option | Type | Required | Description |
15732
+ |--------|------|----------|-------------|
15733
+ | challenge | string | Yes | Base64url-encoded challenge from server |
15734
+ | rp | { id: string; name: string } | Yes | Relying party info |
15735
+ | user | { id: string; name: string; displayName: string } | Yes | User info (id is base64url) |
15736
+ | pubKeyCredParams | PublicKeyCredentialParam[] | No | Defaults to ES256 + RS256 |
15737
+ | timeout | number | No | Timeout in ms (default 60000) |
15738
+ | authenticatorSelection | object | No | Authenticator criteria |
15739
+ | excludeCredentials | CredentialDescriptor[] | No | Prevent re-registration |
15740
+ | attestation | string | No | 'none' \\| 'indirect' \\| 'direct' \\| 'enterprise' |
15741
+
15742
+ **PasskeyCreateResult:**
15743
+
15744
+ | Property | Type | Description |
15745
+ |----------|------|-------------|
15746
+ | id | string | Credential ID (base64url) |
15747
+ | rawId | string | Raw credential ID (base64url) |
15748
+ | type | 'public-key' | Always 'public-key' |
15749
+ | response.clientDataJSON | string | Client data (base64url) |
15750
+ | response.attestationObject | string | Attestation object (base64url) |
15751
+
15752
+ ### getPasskey
15753
+
15754
+ Authenticate with an existing passkey (assertion ceremony).
15755
+
15756
+ \`\`\`tsx
15757
+ await getPasskey(options: PasskeyGetOptions): Promise<PasskeyGetResult>
15758
+ \`\`\`
15759
+
15760
+ **PasskeyGetOptions:**
15761
+
15762
+ | Option | Type | Required | Description |
15763
+ |--------|------|----------|-------------|
15764
+ | challenge | string | Yes | Base64url-encoded challenge from server |
15765
+ | rpId | string | No | Relying party ID |
15766
+ | allowCredentials | CredentialDescriptor[] | No | Allowed credentials (empty = discoverable) |
15767
+ | timeout | number | No | Timeout in ms (default 60000) |
15768
+ | userVerification | string | No | 'required' \\| 'preferred' \\| 'discouraged' |
15769
+
15770
+ **PasskeyGetResult:**
15771
+
15772
+ | Property | Type | Description |
15773
+ |----------|------|-------------|
15774
+ | id | string | Credential ID (base64url) |
15775
+ | rawId | string | Raw credential ID (base64url) |
15776
+ | type | 'public-key' | Always 'public-key' |
15777
+ | response.clientDataJSON | string | Client data (base64url) |
15778
+ | response.authenticatorData | string | Authenticator data (base64url) |
15779
+ | response.signature | string | Signature (base64url) |
15780
+ | response.userHandle | string \\| undefined | User handle (base64url) |
15781
+
15782
+ ### PasskeyError
15783
+
15784
+ Both \`createPasskey\` and \`getPasskey\` throw a \`PasskeyError\` on failure:
15785
+
15786
+ \`\`\`tsx
15787
+ interface PasskeyError {
15788
+ code: 'not_supported' | 'cancelled' | 'invalid_state' | 'not_allowed' | 'unknown';
15789
+ message: string;
15790
+ }
15791
+
15792
+ try {
15793
+ const result = await createPasskey(options);
15794
+ } catch (err) {
15795
+ const passkeyErr = err as PasskeyError;
15796
+ console.log(passkeyErr.code, passkeyErr.message);
15797
+ }
15798
+ \`\`\`
15799
+
15800
+ ---
15801
+
15802
+ ## Base64url Helpers
15803
+
15804
+ Shared utilities for encoding/decoding WebAuthn binary data.
15805
+
15806
+ \`\`\`tsx
15807
+ import { base64urlToBuffer, bufferToBase64url } from '@idealyst/biometrics';
15808
+
15809
+ // Convert base64url string to ArrayBuffer
15810
+ const buffer: ArrayBuffer = base64urlToBuffer(base64urlString);
15811
+
15812
+ // Convert ArrayBuffer to base64url string
15813
+ const str: string = bufferToBase64url(arrayBuffer);
15814
+ \`\`\`
15815
+ `,
15816
+ "idealyst://biometrics/examples": `# Biometrics Examples
15817
+
15818
+ Complete code examples for common @idealyst/biometrics patterns.
15819
+
15820
+ ## Gate Screen Access with Biometrics
15821
+
15822
+ \`\`\`tsx
15823
+ import { isBiometricAvailable, authenticate } from '@idealyst/biometrics';
15824
+ import { useEffect, useState } from 'react';
15825
+
15826
+ function ProtectedScreen({ children }: { children: React.ReactNode }) {
15827
+ const [unlocked, setUnlocked] = useState(false);
15828
+ const [error, setError] = useState<string | null>(null);
15829
+
15830
+ const unlock = async () => {
15831
+ const available = await isBiometricAvailable();
15832
+ if (!available) {
15833
+ // No biometrics \u2014 fall back to PIN or allow access
15834
+ setUnlocked(true);
15835
+ return;
15836
+ }
15837
+
15838
+ const result = await authenticate({
15839
+ promptMessage: 'Unlock to continue',
15840
+ cancelLabel: 'Cancel',
15841
+ });
15842
+
15843
+ if (result.success) {
15844
+ setUnlocked(true);
15845
+ } else {
15846
+ setError(result.message ?? 'Authentication failed');
15847
+ }
15848
+ };
15849
+
15850
+ useEffect(() => {
15851
+ unlock();
15852
+ }, []);
15853
+
15854
+ if (!unlocked) {
15855
+ return (
15856
+ <View>
15857
+ <Text>Please authenticate to continue</Text>
15858
+ {error && <Text intent="negative">{error}</Text>}
15859
+ <Button label="Try Again" onPress={unlock} />
15860
+ </View>
15861
+ );
15862
+ }
15863
+
15864
+ return <>{children}</>;
15865
+ }
15866
+ \`\`\`
15867
+
15868
+ ## Confirm Sensitive Action
15869
+
15870
+ \`\`\`tsx
15871
+ import { authenticate } from '@idealyst/biometrics';
15872
+
15873
+ async function confirmTransfer(amount: number, recipient: string) {
15874
+ const result = await authenticate({
15875
+ promptMessage: \`Confirm transfer of $\${amount} to \${recipient}\`,
15876
+ disableDeviceFallback: false,
15877
+ });
15878
+
15879
+ if (!result.success) {
15880
+ throw new Error(result.message ?? 'Authentication required');
15881
+ }
15882
+
15883
+ // Proceed with transfer
15884
+ await api.transfer({ amount, recipient });
15885
+ }
15886
+ \`\`\`
15887
+
15888
+ ## Show Biometric Info
15889
+
15890
+ \`\`\`tsx
15891
+ import {
15892
+ isBiometricAvailable,
15893
+ getBiometricTypes,
15894
+ getSecurityLevel,
15895
+ } from '@idealyst/biometrics';
15896
+
15897
+ function BiometricSettings() {
15898
+ const [info, setInfo] = useState({
15899
+ available: false,
15900
+ types: [] as string[],
15901
+ level: 'none',
15902
+ });
15903
+
15904
+ useEffect(() => {
15905
+ async function load() {
15906
+ const [available, types, level] = await Promise.all([
15907
+ isBiometricAvailable(),
15908
+ getBiometricTypes(),
15909
+ getSecurityLevel(),
15910
+ ]);
15911
+ setInfo({ available, types, level });
15912
+ }
15913
+ load();
15914
+ }, []);
15915
+
15916
+ return (
15917
+ <Card>
15918
+ <Text>Biometric available: {info.available ? 'Yes' : 'No'}</Text>
15919
+ <Text>Types: {info.types.join(', ') || 'None'}</Text>
15920
+ <Text>Security level: {info.level}</Text>
15921
+ </Card>
15922
+ );
15923
+ }
15924
+ \`\`\`
15925
+
15926
+ ## Passkey Registration Flow
15927
+
15928
+ \`\`\`tsx
15929
+ import { isPasskeySupported, createPasskey } from '@idealyst/biometrics';
15930
+ import type { PasskeyError } from '@idealyst/biometrics';
15931
+
15932
+ async function registerPasskey(user: { id: string; email: string; name: string }) {
15933
+ const supported = await isPasskeySupported();
15934
+ if (!supported) {
15935
+ alert('Passkeys are not supported on this device');
15936
+ return;
15937
+ }
15938
+
15939
+ // 1. Get challenge from server
15940
+ const { challenge, rpId, rpName } = await api.getRegistrationChallenge();
15941
+
15942
+ try {
15943
+ // 2. Create the passkey
15944
+ const credential = await createPasskey({
15945
+ challenge,
15946
+ rp: { id: rpId, name: rpName },
15947
+ user: {
15948
+ id: user.id,
15949
+ name: user.email,
15950
+ displayName: user.name,
15951
+ },
15952
+ authenticatorSelection: {
15953
+ residentKey: 'required',
15954
+ userVerification: 'required',
15955
+ },
15956
+ });
15957
+
15958
+ // 3. Send to server for verification and storage
15959
+ await api.verifyRegistration({
15960
+ id: credential.id,
15961
+ rawId: credential.rawId,
15962
+ clientDataJSON: credential.response.clientDataJSON,
15963
+ attestationObject: credential.response.attestationObject,
15964
+ });
15965
+
15966
+ alert('Passkey registered successfully!');
15967
+ } catch (err) {
15968
+ const passkeyErr = err as PasskeyError;
15969
+ if (passkeyErr.code === 'cancelled') {
15970
+ // User cancelled \u2014 do nothing
15971
+ return;
15972
+ }
15973
+ alert(\`Failed to register passkey: \${passkeyErr.message}\`);
15974
+ }
15975
+ }
15976
+ \`\`\`
15977
+
15978
+ ## Passkey Login Flow
15979
+
15980
+ \`\`\`tsx
15981
+ import { getPasskey } from '@idealyst/biometrics';
15982
+ import type { PasskeyError } from '@idealyst/biometrics';
15983
+
15984
+ async function loginWithPasskey() {
15985
+ // 1. Get challenge from server
15986
+ const { challenge, rpId } = await api.getAuthenticationChallenge();
15987
+
15988
+ try {
15989
+ // 2. Authenticate with passkey
15990
+ const assertion = await getPasskey({
15991
+ challenge,
15992
+ rpId,
15993
+ userVerification: 'required',
15994
+ });
15995
+
15996
+ // 3. Send to server for verification
15997
+ const session = await api.verifyAuthentication({
15998
+ id: assertion.id,
15999
+ rawId: assertion.rawId,
16000
+ clientDataJSON: assertion.response.clientDataJSON,
16001
+ authenticatorData: assertion.response.authenticatorData,
16002
+ signature: assertion.response.signature,
16003
+ userHandle: assertion.response.userHandle,
16004
+ });
16005
+
16006
+ return session;
16007
+ } catch (err) {
16008
+ const passkeyErr = err as PasskeyError;
16009
+ if (passkeyErr.code === 'cancelled') return null;
16010
+ throw passkeyErr;
16011
+ }
16012
+ }
16013
+ \`\`\`
16014
+
16015
+ ## Login Screen with Passkey + Fallback
16016
+
16017
+ \`\`\`tsx
16018
+ import { isPasskeySupported, getPasskey } from '@idealyst/biometrics';
16019
+ import type { PasskeyError } from '@idealyst/biometrics';
16020
+
16021
+ function LoginScreen() {
16022
+ const [passkeyAvailable, setPasskeyAvailable] = useState(false);
16023
+ const [loading, setLoading] = useState(false);
16024
+
16025
+ useEffect(() => {
16026
+ isPasskeySupported().then(setPasskeyAvailable);
16027
+ }, []);
16028
+
16029
+ const handlePasskeyLogin = async () => {
16030
+ setLoading(true);
16031
+ try {
16032
+ const { challenge, rpId } = await api.getAuthenticationChallenge();
16033
+ const assertion = await getPasskey({ challenge, rpId });
16034
+ const session = await api.verifyAuthentication(assertion);
16035
+ navigateToHome(session);
16036
+ } catch (err) {
16037
+ const e = err as PasskeyError;
16038
+ if (e.code !== 'cancelled') {
16039
+ showError(e.message);
16040
+ }
16041
+ } finally {
16042
+ setLoading(false);
16043
+ }
16044
+ };
16045
+
16046
+ return (
16047
+ <View>
16048
+ <Text variant="headline">Welcome Back</Text>
16049
+
16050
+ {passkeyAvailable && (
16051
+ <Button
16052
+ label="Sign in with Passkey"
16053
+ iconName="fingerprint"
16054
+ onPress={handlePasskeyLogin}
16055
+ loading={loading}
16056
+ />
16057
+ )}
16058
+
16059
+ <Button
16060
+ label="Sign in with Email"
16061
+ intent="neutral"
16062
+ onPress={navigateToEmailLogin}
16063
+ />
16064
+ </View>
16065
+ );
16066
+ }
16067
+ \`\`\`
16068
+
16069
+ ## Best Practices
16070
+
16071
+ 1. **Always check availability first** \u2014 Call \`isBiometricAvailable()\` or \`isPasskeySupported()\` before prompting
16072
+ 2. **Provide fallbacks** \u2014 Not all devices support biometrics. Offer PIN/password alternatives
16073
+ 3. **Handle cancellation gracefully** \u2014 Users may cancel the prompt. Don't show errors for \`user_cancel\` / \`cancelled\`
16074
+ 4. **Use try/catch for passkeys** \u2014 Passkey functions throw \`PasskeyError\` on failure
16075
+ 5. **Server-side validation** \u2014 Always verify passkey responses on your server. The client is untrusted
16076
+ 6. **Base64url encoding** \u2014 All binary WebAuthn data is encoded as base64url strings for transport
16077
+ 7. **Set rpId correctly** \u2014 The relying party ID must match your domain. On native, it's your associated domain
16078
+ 8. **Lockout handling** \u2014 After too many failed biometric attempts, the device locks out. Handle the \`lockout\` error
16079
+ 9. **iOS permissions** \u2014 FaceID requires \`NSFaceIDUsageDescription\` in Info.plist
16080
+ 10. **Android associated domains** \u2014 Passkeys on Android require a Digital Asset Links file at \`/.well-known/assetlinks.json\`
16081
+ `
16082
+ };
16083
+
16084
+ // src/data/payments-guides.ts
16085
+ var paymentsGuides = {
16086
+ "idealyst://payments/overview": `# @idealyst/payments Overview
16087
+
16088
+ Cross-platform payment provider abstractions for React and React Native. Wraps Stripe's Platform Pay API for Apple Pay and Google Pay on mobile.
16089
+
16090
+ ## Features
16091
+
16092
+ - **Apple Pay** \u2014 Native iOS payment sheet via Stripe SDK
16093
+ - **Google Pay** \u2014 Native Android payment sheet via Stripe SDK
16094
+ - **Cross-Platform** \u2014 Single API for React Native and web (web is stub/noop)
16095
+ - **Flat Functions + Hook** \u2014 Use \`initializePayments()\`, \`confirmPayment()\` directly, or \`usePayments()\` hook
16096
+ - **Stripe Platform Pay** \u2014 Wraps \`@stripe/stripe-react-native\` Platform Pay API
16097
+ - **Graceful Degradation** \u2014 Falls back cleanly when Stripe SDK isn't installed
16098
+ - **TypeScript** \u2014 Full type safety
16099
+
16100
+ ## Installation
16101
+
16102
+ \`\`\`bash
16103
+ yarn add @idealyst/payments
16104
+
16105
+ # React Native \u2014 required for mobile payments:
16106
+ yarn add @stripe/stripe-react-native
16107
+ cd ios && pod install
16108
+ \`\`\`
16109
+
16110
+ ## Web Support
16111
+
16112
+ Web provides a functional stub \u2014 \`initializePayments()\` succeeds but all payment methods report as unavailable. Payment actions throw descriptive errors pointing to Stripe Elements (\`@stripe/react-stripe-js\`).
16113
+
16114
+ ## Quick Start
16115
+
16116
+ \`\`\`tsx
16117
+ import { usePayments } from '@idealyst/payments';
16118
+
16119
+ function CheckoutScreen() {
16120
+ const {
16121
+ isReady,
16122
+ isApplePayAvailable,
16123
+ isGooglePayAvailable,
16124
+ isProcessing,
16125
+ confirmPayment,
16126
+ } = usePayments({
16127
+ config: {
16128
+ publishableKey: 'pk_test_xxx',
16129
+ merchantIdentifier: 'merchant.com.myapp',
16130
+ merchantName: 'My App',
16131
+ merchantCountryCode: 'US',
16132
+ },
16133
+ });
16134
+
16135
+ const handlePay = async () => {
16136
+ const result = await confirmPayment({
16137
+ clientSecret: 'pi_xxx_secret_xxx', // from server
16138
+ amount: { amount: 1099, currencyCode: 'usd' },
16139
+ });
16140
+ console.log('Paid:', result.paymentIntentId);
16141
+ };
16142
+
16143
+ if (!isReady) return null;
16144
+
16145
+ return (
16146
+ <Button
16147
+ onPress={handlePay}
16148
+ disabled={isProcessing}
16149
+ label={isApplePayAvailable ? 'Apple Pay' : 'Google Pay'}
16150
+ />
16151
+ );
16152
+ }
16153
+ \`\`\`
16154
+
16155
+ ## Platform Support
16156
+
16157
+ | Feature | iOS | Android | Web |
16158
+ |---------|-----|---------|-----|
16159
+ | Apple Pay | Yes | \u2014 | \u2014 |
16160
+ | Google Pay | \u2014 | Yes | \u2014 |
16161
+ | usePayments hook | Yes | Yes | Stub |
16162
+ | confirmPayment | Yes | Yes | Throws |
16163
+ | createPaymentMethod | Yes | Yes | Throws |
16164
+ `,
16165
+ "idealyst://payments/api": `# Payments API Reference
16166
+
16167
+ Complete API reference for @idealyst/payments.
16168
+
16169
+ ---
16170
+
16171
+ ## Flat Functions
16172
+
16173
+ ### initializePayments
16174
+
16175
+ Initialize the Stripe SDK for platform payments.
16176
+
16177
+ \`\`\`tsx
16178
+ import { initializePayments } from '@idealyst/payments';
16179
+
16180
+ await initializePayments({
16181
+ publishableKey: 'pk_test_xxx',
16182
+ merchantIdentifier: 'merchant.com.myapp', // iOS Apple Pay
16183
+ merchantName: 'My App',
16184
+ merchantCountryCode: 'US',
16185
+ urlScheme: 'com.myapp', // for 3D Secure redirects
16186
+ testEnvironment: true, // Google Pay test mode
16187
+ });
16188
+ \`\`\`
16189
+
16190
+ **PaymentConfig:**
16191
+
16192
+ | Option | Type | Required | Description |
16193
+ |--------|------|----------|-------------|
16194
+ | publishableKey | string | Yes | Stripe publishable key |
16195
+ | merchantIdentifier | string | No | Apple Pay merchant ID (iOS) |
16196
+ | merchantName | string | Yes | Display name on payment sheet |
16197
+ | merchantCountryCode | string | Yes | ISO 3166-1 alpha-2 country |
16198
+ | urlScheme | string | No | URL scheme for 3D Secure |
16199
+ | testEnvironment | boolean | No | Google Pay test mode (default: false) |
16200
+
16201
+ ### checkPaymentAvailability
16202
+
16203
+ Check which payment methods are available on the current device.
16204
+
16205
+ \`\`\`tsx
16206
+ import { checkPaymentAvailability } from '@idealyst/payments';
16207
+
16208
+ const methods = await checkPaymentAvailability();
16209
+ // [
16210
+ // { type: 'apple_pay', isAvailable: true },
16211
+ // { type: 'google_pay', isAvailable: false, unavailableReason: '...' },
16212
+ // { type: 'card', isAvailable: true },
16213
+ // ]
16214
+ \`\`\`
16215
+
16216
+ ### confirmPayment
16217
+
16218
+ Present the platform payment sheet and confirm a PaymentIntent. Requires a \`clientSecret\` from a server-created PaymentIntent.
16219
+
16220
+ \`\`\`tsx
16221
+ import { confirmPayment } from '@idealyst/payments';
16222
+
16223
+ const result = await confirmPayment({
16224
+ clientSecret: 'pi_xxx_secret_xxx',
16225
+ amount: { amount: 1099, currencyCode: 'usd' },
16226
+ lineItems: [
16227
+ { label: 'Widget', amount: 999 },
16228
+ { label: 'Tax', amount: 100 },
16229
+ ],
16230
+ billingAddress: {
16231
+ isRequired: true,
16232
+ format: 'FULL',
16233
+ },
16234
+ });
16235
+
16236
+ console.log(result.paymentIntentId); // 'pi_xxx'
16237
+ console.log(result.status); // 'succeeded'
16238
+ \`\`\`
16239
+
16240
+ ### createPaymentMethod
16241
+
16242
+ Present the payment sheet and create a payment method without confirming. Returns a payment method ID for server-side processing.
16243
+
16244
+ \`\`\`tsx
16245
+ import { createPaymentMethod } from '@idealyst/payments';
16246
+
16247
+ const result = await createPaymentMethod({
16248
+ amount: { amount: 1099, currencyCode: 'usd' },
16249
+ });
16250
+
16251
+ console.log(result.paymentMethodId); // 'pm_xxx'
16252
+ // Send to your server for processing
16253
+ \`\`\`
16254
+
16255
+ ### getPaymentStatus
16256
+
16257
+ Get the current payment provider status.
16258
+
16259
+ \`\`\`tsx
16260
+ import { getPaymentStatus } from '@idealyst/payments';
16261
+
16262
+ const status = getPaymentStatus();
16263
+ // { state: 'ready', availablePaymentMethods: [...], isPaymentAvailable: true }
16264
+ \`\`\`
16265
+
16266
+ ---
16267
+
16268
+ ## usePayments Hook
16269
+
16270
+ Convenience hook that wraps the flat functions with React state management.
16271
+
16272
+ \`\`\`tsx
16273
+ import { usePayments } from '@idealyst/payments';
16274
+
16275
+ const {
16276
+ // State
16277
+ status, // PaymentProviderStatus
16278
+ isReady, // boolean
16279
+ isProcessing, // boolean
16280
+ availablePaymentMethods, // PaymentMethodAvailability[]
16281
+ isApplePayAvailable, // boolean
16282
+ isGooglePayAvailable,// boolean
16283
+ isPaymentAvailable, // boolean
16284
+ error, // PaymentError | null
16285
+
16286
+ // Actions
16287
+ initialize, // (config: PaymentConfig) => Promise<void>
16288
+ checkAvailability, // () => Promise<PaymentMethodAvailability[]>
16289
+ confirmPayment, // (request: PaymentSheetRequest) => Promise<PaymentResult>
16290
+ createPaymentMethod, // (request: PaymentSheetRequest) => Promise<PaymentResult>
16291
+ clearError, // () => void
16292
+ } = usePayments(options?);
16293
+ \`\`\`
16294
+
16295
+ **UsePaymentsOptions:**
16296
+
16297
+ | Option | Type | Default | Description |
16298
+ |--------|------|---------|-------------|
16299
+ | config | PaymentConfig | \u2014 | Auto-initialize with this config |
16300
+ | autoCheckAvailability | boolean | true | Check availability after init |
16301
+
16302
+ ---
16303
+
16304
+ ## Types
16305
+
16306
+ ### PaymentMethodType
16307
+
16308
+ \`\`\`tsx
16309
+ type PaymentMethodType = 'apple_pay' | 'google_pay' | 'card';
16310
+ \`\`\`
16311
+
16312
+ ### PaymentAmount
14428
16313
 
14429
16314
  \`\`\`tsx
14430
- import React from 'react';
14431
- import { View, Text } from '@idealyst/components';
14432
- import { LineChart } from '@idealyst/charts';
14433
- import type { ChartDataSeries } from '@idealyst/charts';
16315
+ interface PaymentAmount {
16316
+ amount: number; // smallest currency unit (cents)
16317
+ currencyCode: string; // ISO 4217 (e.g., 'usd')
16318
+ }
16319
+ \`\`\`
14434
16320
 
14435
- const monthlySales = [
14436
- { x: 'Jan', y: 4200 },
14437
- { x: 'Feb', y: 5100 },
14438
- { x: 'Mar', y: 4800 },
14439
- { x: 'Apr', y: 6200 },
14440
- { x: 'May', y: 5900 },
14441
- { x: 'Jun', y: 7100 },
14442
- ];
16321
+ ### PaymentSheetRequest
14443
16322
 
14444
- // Series uses 'name' for display \u2014 NOT 'label'
14445
- const revenueData: ChartDataSeries[] = [
14446
- { id: 'revenue', name: 'Revenue', data: monthlySales, color: '#4CAF50' },
14447
- ];
16323
+ \`\`\`tsx
16324
+ interface PaymentSheetRequest {
16325
+ clientSecret?: string; // required for confirmPayment
16326
+ amount: PaymentAmount;
16327
+ lineItems?: PaymentLineItem[];
16328
+ billingAddress?: BillingAddressConfig;
16329
+ allowedPaymentMethods?: PaymentMethodType[];
16330
+ }
16331
+ \`\`\`
14448
16332
 
14449
- function SalesOverview() {
14450
- return (
14451
- <View padding="md" gap="md">
14452
- <Text typography="h6" weight="bold">Monthly Sales</Text>
14453
- <LineChart
14454
- data={revenueData}
14455
- height={300}
14456
- curve="monotone"
14457
- showDots
14458
- showArea
14459
- areaOpacity={0.15}
14460
- animate
14461
- xAxis={{ label: 'Month' }}
14462
- yAxis={{
14463
- label: 'Revenue ($)',
14464
- // tickFormat param type is (value: number | string | Date) => string
14465
- tickFormat: (value: number | string | Date) => \`$\${Number(value) / 1000}k\`,
14466
- }}
14467
- />
14468
- </View>
14469
- );
16333
+ ### PaymentResult
16334
+
16335
+ \`\`\`tsx
16336
+ interface PaymentResult {
16337
+ paymentMethodType: PaymentMethodType;
16338
+ paymentMethodId?: string; // for createPaymentMethod
16339
+ paymentIntentId?: string; // for confirmPayment
16340
+ status?: string; // 'succeeded', 'requires_capture', etc.
14470
16341
  }
14471
16342
  \`\`\`
14472
16343
 
14473
- ## Multi-Series Line Chart
16344
+ ### PaymentError
14474
16345
 
14475
16346
  \`\`\`tsx
14476
- import React from 'react';
14477
- import { LineChart } from '@idealyst/charts';
14478
- import type { ChartDataSeries } from '@idealyst/charts';
16347
+ interface PaymentError {
16348
+ code: PaymentErrorCode;
16349
+ message: string;
16350
+ originalError?: unknown;
16351
+ }
14479
16352
 
14480
- // Each series has: id, name, data, color? \u2014 NO 'label' property
14481
- const series: ChartDataSeries[] = [
14482
- {
14483
- id: 'product-a',
14484
- name: 'Product A', // Use 'name' \u2014 NOT 'label'
14485
- data: [
14486
- { x: 'Q1', y: 120 },
14487
- { x: 'Q2', y: 150 },
14488
- { x: 'Q3', y: 180 },
14489
- { x: 'Q4', y: 210 },
14490
- ],
14491
- color: '#2196F3',
14492
- },
14493
- {
14494
- id: 'product-b',
14495
- name: 'Product B', // Use 'name' \u2014 NOT 'label'
14496
- data: [
14497
- { x: 'Q1', y: 80 },
14498
- { x: 'Q2', y: 110 },
14499
- { x: 'Q3', y: 95 },
14500
- { x: 'Q4', y: 140 },
14501
- ],
14502
- color: '#FF9800',
14503
- },
14504
- ];
16353
+ type PaymentErrorCode =
16354
+ | 'not_initialized'
16355
+ | 'not_available'
16356
+ | 'not_supported'
16357
+ | 'user_cancelled'
16358
+ | 'payment_failed'
16359
+ | 'network_error'
16360
+ | 'invalid_config'
16361
+ | 'invalid_request'
16362
+ | 'provider_error'
16363
+ | 'unknown';
16364
+ \`\`\`
16365
+ `,
16366
+ "idealyst://payments/examples": `# Payments Examples
16367
+
16368
+ Complete code examples for common @idealyst/payments patterns.
16369
+
16370
+ ## Basic Checkout with Hook
16371
+
16372
+ \`\`\`tsx
16373
+ import { usePayments } from '@idealyst/payments';
16374
+ import { Button, View, Text } from '@idealyst/components';
16375
+
16376
+ function CheckoutScreen({ clientSecret }: { clientSecret: string }) {
16377
+ const {
16378
+ isReady,
16379
+ isApplePayAvailable,
16380
+ isGooglePayAvailable,
16381
+ isProcessing,
16382
+ error,
16383
+ confirmPayment,
16384
+ clearError,
16385
+ } = usePayments({
16386
+ config: {
16387
+ publishableKey: 'pk_test_xxx',
16388
+ merchantIdentifier: 'merchant.com.myapp',
16389
+ merchantName: 'My App',
16390
+ merchantCountryCode: 'US',
16391
+ testEnvironment: __DEV__,
16392
+ },
16393
+ });
16394
+
16395
+ const handlePay = async () => {
16396
+ try {
16397
+ const result = await confirmPayment({
16398
+ clientSecret,
16399
+ amount: { amount: 1099, currencyCode: 'usd' },
16400
+ lineItems: [
16401
+ { label: 'Widget', amount: 999 },
16402
+ { label: 'Tax', amount: 100 },
16403
+ ],
16404
+ });
16405
+
16406
+ // Payment succeeded
16407
+ console.log('Payment confirmed:', result.paymentIntentId);
16408
+ } catch (err) {
16409
+ // Error is automatically set in hook state
16410
+ console.error('Payment failed:', err);
16411
+ }
16412
+ };
16413
+
16414
+ if (!isReady) {
16415
+ return <Text>Loading payment methods...</Text>;
16416
+ }
16417
+
16418
+ const canPay = isApplePayAvailable || isGooglePayAvailable;
14505
16419
 
14506
- function ComparisonChart() {
14507
16420
  return (
14508
- <LineChart
14509
- data={series}
14510
- height={350}
14511
- curve="monotone"
14512
- showDots
14513
- animate
14514
- />
16421
+ <View>
16422
+ {canPay ? (
16423
+ <Button
16424
+ onPress={handlePay}
16425
+ disabled={isProcessing}
16426
+ intent="primary"
16427
+ >
16428
+ {isApplePayAvailable ? 'Pay with Apple Pay' : 'Pay with Google Pay'}
16429
+ </Button>
16430
+ ) : (
16431
+ <Text>No payment methods available</Text>
16432
+ )}
16433
+
16434
+ {error && (
16435
+ <View>
16436
+ <Text intent="danger">{error.message}</Text>
16437
+ <Button onPress={clearError}>Dismiss</Button>
16438
+ </View>
16439
+ )}
16440
+ </View>
14515
16441
  );
14516
16442
  }
14517
16443
  \`\`\`
14518
16444
 
14519
- ## Bar Chart
16445
+ ## Create Payment Method (Server-Side Confirm)
14520
16446
 
14521
16447
  \`\`\`tsx
14522
- import React from 'react';
14523
- import { View, Text } from '@idealyst/components';
14524
- import { BarChart } from '@idealyst/charts';
16448
+ import { usePayments } from '@idealyst/payments';
16449
+
16450
+ function DonateScreen() {
16451
+ const { isReady, isPaymentAvailable, createPaymentMethod } = usePayments({
16452
+ config: {
16453
+ publishableKey: 'pk_test_xxx',
16454
+ merchantName: 'Charity',
16455
+ merchantCountryCode: 'US',
16456
+ },
16457
+ });
14525
16458
 
14526
- const categories = [
14527
- { x: 'Electronics', y: 45 },
14528
- { x: 'Clothing', y: 32 },
14529
- { x: 'Books', y: 18 },
14530
- { x: 'Food', y: 56 },
14531
- { x: 'Sports', y: 28 },
14532
- ];
16459
+ const handleDonate = async (amountCents: number) => {
16460
+ const result = await createPaymentMethod({
16461
+ amount: { amount: amountCents, currencyCode: 'usd' },
16462
+ });
16463
+
16464
+ // Send payment method to your server
16465
+ await fetch('/api/donate', {
16466
+ method: 'POST',
16467
+ body: JSON.stringify({
16468
+ paymentMethodId: result.paymentMethodId,
16469
+ amount: amountCents,
16470
+ }),
16471
+ });
16472
+ };
14533
16473
 
14534
- function CategoryBreakdown() {
14535
16474
  return (
14536
- <View padding="md" gap="md">
14537
- <Text typography="h6" weight="bold">Sales by Category</Text>
14538
- <BarChart
14539
- data={[{ id: 'units', name: 'Units Sold', data: categories }]}
14540
- height={300}
14541
- barRadius={4}
14542
- animate
14543
- yAxis={{ tickFormat: (value: number | string | Date) => \`\${value} units\` }}
14544
- />
16475
+ <View>
16476
+ <Button onPress={() => handleDonate(500)} disabled={!isPaymentAvailable}>
16477
+ Donate $5
16478
+ </Button>
16479
+ <Button onPress={() => handleDonate(1000)} disabled={!isPaymentAvailable}>
16480
+ Donate $10
16481
+ </Button>
14545
16482
  </View>
14546
16483
  );
14547
16484
  }
14548
16485
  \`\`\`
14549
16486
 
14550
- ## Stacked Bar Chart
16487
+ ## Flat Functions (No Hook)
14551
16488
 
14552
16489
  \`\`\`tsx
14553
- import React from 'react';
14554
- import { BarChart } from '@idealyst/charts';
16490
+ import {
16491
+ initializePayments,
16492
+ checkPaymentAvailability,
16493
+ confirmPayment,
16494
+ getPaymentStatus,
16495
+ } from '@idealyst/payments';
16496
+ import type { PaymentError } from '@idealyst/payments';
16497
+
16498
+ // Initialize once at app startup
16499
+ async function setupPayments() {
16500
+ await initializePayments({
16501
+ publishableKey: 'pk_test_xxx',
16502
+ merchantIdentifier: 'merchant.com.myapp',
16503
+ merchantName: 'My App',
16504
+ merchantCountryCode: 'US',
16505
+ });
14555
16506
 
14556
- function StackedBarExample() {
14557
- return (
14558
- <BarChart
14559
- data={[
14560
- {
14561
- id: 'online',
14562
- name: 'Online',
14563
- data: [
14564
- { x: 'Q1', y: 100 },
14565
- { x: 'Q2', y: 120 },
14566
- { x: 'Q3', y: 90 },
14567
- ],
14568
- color: '#4CAF50',
14569
- },
14570
- {
14571
- id: 'in-store',
14572
- name: 'In-Store',
14573
- data: [
14574
- { x: 'Q1', y: 60 },
14575
- { x: 'Q2', y: 80 },
14576
- { x: 'Q3', y: 70 },
14577
- ],
14578
- color: '#2196F3',
14579
- },
14580
- ]}
14581
- height={300}
14582
- stacked
14583
- animate
14584
- />
14585
- );
16507
+ const status = getPaymentStatus();
16508
+ console.log('Payment ready:', status.state === 'ready');
16509
+ }
16510
+
16511
+ // Check availability
16512
+ async function canPay() {
16513
+ const methods = await checkPaymentAvailability();
16514
+ return methods.some(m => m.isAvailable);
16515
+ }
16516
+
16517
+ // Process payment
16518
+ async function processPayment(clientSecret: string, totalCents: number) {
16519
+ try {
16520
+ const result = await confirmPayment({
16521
+ clientSecret,
16522
+ amount: { amount: totalCents, currencyCode: 'usd' },
16523
+ });
16524
+ return result;
16525
+ } catch (err) {
16526
+ const paymentErr = err as PaymentError;
16527
+ if (paymentErr.code === 'user_cancelled') {
16528
+ return null; // User cancelled \u2014 not an error
16529
+ }
16530
+ throw paymentErr;
16531
+ }
14586
16532
  }
14587
16533
  \`\`\`
14588
16534
 
14589
- ## Horizontal Bar Chart
16535
+ ## Platform-Conditional UI
14590
16536
 
14591
16537
  \`\`\`tsx
14592
- import React from 'react';
14593
- import { BarChart } from '@idealyst/charts';
16538
+ import { usePayments } from '@idealyst/payments';
16539
+ import { Platform } from 'react-native';
14594
16540
 
14595
- function HorizontalBarExample() {
14596
- const data = [
14597
- { x: 'React', y: 85 },
14598
- { x: 'Vue', y: 62 },
14599
- { x: 'Angular', y: 45 },
14600
- { x: 'Svelte', y: 38 },
14601
- ];
16541
+ function PaymentButtons({ clientSecret }: { clientSecret: string }) {
16542
+ const {
16543
+ isApplePayAvailable,
16544
+ isGooglePayAvailable,
16545
+ confirmPayment,
16546
+ isProcessing,
16547
+ } = usePayments({
16548
+ config: {
16549
+ publishableKey: 'pk_test_xxx',
16550
+ merchantName: 'My Store',
16551
+ merchantCountryCode: 'US',
16552
+ },
16553
+ });
16554
+
16555
+ const handlePlatformPay = () =>
16556
+ confirmPayment({
16557
+ clientSecret,
16558
+ amount: { amount: 2499, currencyCode: 'usd' },
16559
+ });
14602
16560
 
14603
16561
  return (
14604
- <BarChart
14605
- data={[{ id: 'popularity', name: 'Popularity', data }]}
14606
- height={250}
14607
- orientation="horizontal"
14608
- animate
14609
- />
16562
+ <View>
16563
+ {isApplePayAvailable && (
16564
+ <Button
16565
+ onPress={handlePlatformPay}
16566
+ disabled={isProcessing}
16567
+ iconName="apple"
16568
+ >
16569
+ Apple Pay
16570
+ </Button>
16571
+ )}
16572
+
16573
+ {isGooglePayAvailable && (
16574
+ <Button
16575
+ onPress={handlePlatformPay}
16576
+ disabled={isProcessing}
16577
+ iconName="google"
16578
+ >
16579
+ Google Pay
16580
+ </Button>
16581
+ )}
16582
+
16583
+ {/* Always show a card fallback */}
16584
+ <Button
16585
+ onPress={() => navigateToCardForm()}
16586
+ intent="secondary"
16587
+ >
16588
+ Pay with Card
16589
+ </Button>
16590
+ </View>
14610
16591
  );
14611
16592
  }
14612
16593
  \`\`\`
16594
+
16595
+ ## Best Practices
16596
+
16597
+ 1. **Server-side PaymentIntent** \u2014 Always create PaymentIntents on your server, never on the client
16598
+ 2. **Handle cancellation** \u2014 \`user_cancelled\` is not an error, don't show error UI for it
16599
+ 3. **Test environment** \u2014 Set \`testEnvironment: true\` during development for Google Pay
16600
+ 4. **Apple Pay merchant ID** \u2014 Requires Apple Developer Program and Xcode capability setup
16601
+ 5. **Amounts in cents** \u2014 All amounts are in the smallest currency unit (1099 = $10.99)
16602
+ 6. **Web fallback** \u2014 On web, use \`@stripe/react-stripe-js\` (Stripe Elements) directly
16603
+ 7. **3D Secure** \u2014 Set \`urlScheme\` in config for 3D Secure / bank redirect flows on native
16604
+ 8. **Error handling** \u2014 Always wrap \`confirmPayment\` / \`createPaymentMethod\` in try/catch
14613
16605
  `
14614
16606
  };
14615
16607
 
@@ -24323,7 +26315,7 @@ function getStorageGuide(args) {
24323
26315
  const guide = storageGuides[uri];
24324
26316
  if (!guide) {
24325
26317
  return textResponse(
24326
- `Topic "${topic}" not found. Available topics: overview, api, examples`
26318
+ `Topic "${topic}" not found. Available topics: overview, api, examples, secure`
24327
26319
  );
24328
26320
  }
24329
26321
  return textResponse(guide);
@@ -24449,6 +26441,39 @@ function getChartsGuide(args) {
24449
26441
  }
24450
26442
  return textResponse(guide);
24451
26443
  }
26444
+ function getClipboardGuide(args) {
26445
+ const topic = args.topic;
26446
+ const uri = `idealyst://clipboard/${topic}`;
26447
+ const guide = clipboardGuides[uri];
26448
+ if (!guide) {
26449
+ return textResponse(
26450
+ `Topic "${topic}" not found. Available topics: overview, api, examples`
26451
+ );
26452
+ }
26453
+ return textResponse(guide);
26454
+ }
26455
+ function getBiometricsGuide(args) {
26456
+ const topic = args.topic;
26457
+ const uri = `idealyst://biometrics/${topic}`;
26458
+ const guide = biometricsGuides[uri];
26459
+ if (!guide) {
26460
+ return textResponse(
26461
+ `Topic "${topic}" not found. Available topics: overview, api, examples`
26462
+ );
26463
+ }
26464
+ return textResponse(guide);
26465
+ }
26466
+ function getPaymentsGuide(args) {
26467
+ const topic = args.topic;
26468
+ const uri = `idealyst://payments/${topic}`;
26469
+ const guide = paymentsGuides[uri];
26470
+ if (!guide) {
26471
+ return textResponse(
26472
+ `Topic "${topic}" not found. Available topics: overview, api, examples`
26473
+ );
26474
+ }
26475
+ return textResponse(guide);
26476
+ }
24452
26477
  function listPackages(args = {}) {
24453
26478
  const category = args.category;
24454
26479
  if (category) {
@@ -24694,6 +26719,9 @@ var toolHandlers = {
24694
26719
  get_markdown_guide: getMarkdownGuide,
24695
26720
  get_config_guide: getConfigGuide,
24696
26721
  get_charts_guide: getChartsGuide,
26722
+ get_clipboard_guide: getClipboardGuide,
26723
+ get_biometrics_guide: getBiometricsGuide,
26724
+ get_payments_guide: getPaymentsGuide,
24697
26725
  list_packages: listPackages,
24698
26726
  get_package_docs: getPackageDocs,
24699
26727
  search_packages: searchPackages2,