@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/{chunk-NX77GGPR.js → chunk-7WPOVADU.js} +2152 -118
- package/dist/chunk-7WPOVADU.js.map +1 -0
- package/dist/generated/types.json +41107 -0
- package/dist/index.cjs +2183 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/index.cjs +2157 -117
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.d.cts +28 -4
- package/dist/tools/index.d.ts +28 -4
- package/dist/tools/index.js +13 -1
- package/package.json +5 -5
- package/dist/chunk-NX77GGPR.js.map +0 -1
|
@@ -187,8 +187,8 @@ var getStorageGuideDefinition = {
|
|
|
187
187
|
properties: {
|
|
188
188
|
topic: {
|
|
189
189
|
type: "string",
|
|
190
|
-
description: "Topic to get docs for: 'overview', 'api', 'examples'",
|
|
191
|
-
enum: ["overview", "api", "examples"]
|
|
190
|
+
description: "Topic to get docs for: 'overview', 'api', 'examples', 'secure'",
|
|
191
|
+
enum: ["overview", "api", "examples", "secure"]
|
|
192
192
|
}
|
|
193
193
|
},
|
|
194
194
|
required: ["topic"]
|
|
@@ -359,6 +359,51 @@ var getChartsGuideDefinition = {
|
|
|
359
359
|
required: ["topic"]
|
|
360
360
|
}
|
|
361
361
|
};
|
|
362
|
+
var getClipboardGuideDefinition = {
|
|
363
|
+
name: "get_clipboard_guide",
|
|
364
|
+
description: "Get documentation for @idealyst/clipboard cross-platform clipboard and OTP autofill package. Covers copy/paste API, useOTPAutoFill hook, and examples.",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
type: "object",
|
|
367
|
+
properties: {
|
|
368
|
+
topic: {
|
|
369
|
+
type: "string",
|
|
370
|
+
description: "Topic to get docs for: 'overview', 'api', 'examples'",
|
|
371
|
+
enum: ["overview", "api", "examples"]
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
required: ["topic"]
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var getBiometricsGuideDefinition = {
|
|
378
|
+
name: "get_biometrics_guide",
|
|
379
|
+
description: "Get documentation for @idealyst/biometrics cross-platform biometric authentication and passkeys (WebAuthn/FIDO2) package. Covers local biometric auth, passkey registration/login, and examples.",
|
|
380
|
+
inputSchema: {
|
|
381
|
+
type: "object",
|
|
382
|
+
properties: {
|
|
383
|
+
topic: {
|
|
384
|
+
type: "string",
|
|
385
|
+
description: "Topic to get docs for: 'overview', 'api', 'examples'",
|
|
386
|
+
enum: ["overview", "api", "examples"]
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
required: ["topic"]
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
var getPaymentsGuideDefinition = {
|
|
393
|
+
name: "get_payments_guide",
|
|
394
|
+
description: "Get documentation for @idealyst/payments cross-platform payment provider package. Covers Apple Pay, Google Pay, Stripe Platform Pay, usePayments hook, and examples.",
|
|
395
|
+
inputSchema: {
|
|
396
|
+
type: "object",
|
|
397
|
+
properties: {
|
|
398
|
+
topic: {
|
|
399
|
+
type: "string",
|
|
400
|
+
description: "Topic to get docs for: 'overview', 'api', 'examples'",
|
|
401
|
+
enum: ["overview", "api", "examples"]
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
required: ["topic"]
|
|
405
|
+
}
|
|
406
|
+
};
|
|
362
407
|
var listPackagesDefinition = {
|
|
363
408
|
name: "list_packages",
|
|
364
409
|
description: "List all available Idealyst packages with descriptions, categories, and documentation status. Use this to discover what packages are available in the framework.",
|
|
@@ -513,6 +558,9 @@ var toolDefinitions = [
|
|
|
513
558
|
getMarkdownGuideDefinition,
|
|
514
559
|
getConfigGuideDefinition,
|
|
515
560
|
getChartsGuideDefinition,
|
|
561
|
+
getClipboardGuideDefinition,
|
|
562
|
+
getBiometricsGuideDefinition,
|
|
563
|
+
getPaymentsGuideDefinition,
|
|
516
564
|
// Package tools
|
|
517
565
|
listPackagesDefinition,
|
|
518
566
|
getPackageDocsDefinition,
|
|
@@ -2335,6 +2383,7 @@ Cross-platform storage solution for React and React Native applications. Provide
|
|
|
2335
2383
|
- **React Native** - Uses MMKV for high-performance storage
|
|
2336
2384
|
- **Web** - Uses localStorage with proper error handling
|
|
2337
2385
|
- **TypeScript** - Full type safety and IntelliSense support
|
|
2386
|
+
- **Secure Storage** - Optional encrypted storage via \`createSecureStorage()\` (Keychain on native, Web Crypto on web)
|
|
2338
2387
|
|
|
2339
2388
|
## Installation
|
|
2340
2389
|
|
|
@@ -2738,6 +2787,127 @@ async function safeStorageOperation() {
|
|
|
2738
2787
|
6. **Data Size** - Keep stored objects reasonably sized
|
|
2739
2788
|
7. **Cleanup** - Periodically clean up unused data
|
|
2740
2789
|
8. **Type Safety** - Create typed wrapper functions for better TypeScript support
|
|
2790
|
+
9. **Use Secure Storage for Secrets** - Use \`createSecureStorage()\` for auth tokens, API keys, and sensitive data
|
|
2791
|
+
`,
|
|
2792
|
+
"idealyst://storage/secure": `# Secure Storage
|
|
2793
|
+
|
|
2794
|
+
Encrypted storage for sensitive data like auth tokens, API keys, and secrets. Uses the same \`IStorage\` interface as regular storage \u2014 drop-in replacement.
|
|
2795
|
+
|
|
2796
|
+
## Installation
|
|
2797
|
+
|
|
2798
|
+
\`\`\`bash
|
|
2799
|
+
yarn add @idealyst/storage
|
|
2800
|
+
|
|
2801
|
+
# React Native also needs (for secure storage):
|
|
2802
|
+
yarn add react-native-keychain react-native-mmkv
|
|
2803
|
+
cd ios && pod install
|
|
2804
|
+
\`\`\`
|
|
2805
|
+
|
|
2806
|
+
## Quick Start
|
|
2807
|
+
|
|
2808
|
+
\`\`\`tsx
|
|
2809
|
+
import { createSecureStorage } from '@idealyst/storage';
|
|
2810
|
+
|
|
2811
|
+
// Create a secure storage instance
|
|
2812
|
+
const secureStorage = createSecureStorage();
|
|
2813
|
+
|
|
2814
|
+
// Same API as regular storage
|
|
2815
|
+
await secureStorage.setItem('authToken', 'eyJhbGciOiJIUzI1NiIs...');
|
|
2816
|
+
const token = await secureStorage.getItem('authToken');
|
|
2817
|
+
await secureStorage.removeItem('authToken');
|
|
2818
|
+
await secureStorage.clear();
|
|
2819
|
+
const keys = await secureStorage.getAllKeys();
|
|
2820
|
+
|
|
2821
|
+
// Listeners work too
|
|
2822
|
+
const unsubscribe = secureStorage.addListener((key, value) => {
|
|
2823
|
+
console.log('Secure storage changed:', key);
|
|
2824
|
+
});
|
|
2825
|
+
\`\`\`
|
|
2826
|
+
|
|
2827
|
+
## Options
|
|
2828
|
+
|
|
2829
|
+
\`\`\`tsx
|
|
2830
|
+
import { createSecureStorage, SecureStorageOptions } from '@idealyst/storage';
|
|
2831
|
+
|
|
2832
|
+
const secureStorage = createSecureStorage({
|
|
2833
|
+
prefix: 'myapp', // Namespace for keys (default: 'secure')
|
|
2834
|
+
});
|
|
2835
|
+
\`\`\`
|
|
2836
|
+
|
|
2837
|
+
The \`prefix\` option controls:
|
|
2838
|
+
- **Native**: Keychain service name and MMKV instance ID
|
|
2839
|
+
- **Web**: localStorage key prefix and IndexedDB key name
|
|
2840
|
+
|
|
2841
|
+
Use different prefixes to create isolated secure storage instances.
|
|
2842
|
+
|
|
2843
|
+
## How It Works
|
|
2844
|
+
|
|
2845
|
+
### React Native
|
|
2846
|
+
1. A random 16-byte encryption key is generated on first use
|
|
2847
|
+
2. The key is stored in the **iOS Keychain** / **Android Keystore** (hardware-backed)
|
|
2848
|
+
3. An encrypted MMKV instance is created using that key
|
|
2849
|
+
4. All data is encrypted at rest by MMKV's native AES encryption
|
|
2850
|
+
5. Keychain accessibility is set to \`WHEN_UNLOCKED_THIS_DEVICE_ONLY\` (not backed up, only accessible when device is unlocked)
|
|
2851
|
+
|
|
2852
|
+
### Web
|
|
2853
|
+
1. A non-extractable AES-256-GCM \`CryptoKey\` is generated on first use
|
|
2854
|
+
2. The key is stored in **IndexedDB** (non-extractable \u2014 cannot be read as raw bytes)
|
|
2855
|
+
3. Each value is encrypted with a unique random IV before storing in localStorage
|
|
2856
|
+
4. Requires a **secure context** (HTTPS) for \`crypto.subtle\` access
|
|
2857
|
+
|
|
2858
|
+
## Usage Example: Secure Auth Service
|
|
2859
|
+
|
|
2860
|
+
\`\`\`tsx
|
|
2861
|
+
import { createSecureStorage } from '@idealyst/storage';
|
|
2862
|
+
|
|
2863
|
+
const secureStorage = createSecureStorage({ prefix: 'auth' });
|
|
2864
|
+
|
|
2865
|
+
interface AuthTokens {
|
|
2866
|
+
accessToken: string;
|
|
2867
|
+
refreshToken: string;
|
|
2868
|
+
expiresAt: number;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
class SecureAuthService {
|
|
2872
|
+
static async saveTokens(tokens: AuthTokens) {
|
|
2873
|
+
await secureStorage.setItem('tokens', JSON.stringify(tokens));
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
static async getTokens(): Promise<AuthTokens | null> {
|
|
2877
|
+
const data = await secureStorage.getItem('tokens');
|
|
2878
|
+
return data ? JSON.parse(data) as AuthTokens : null;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
static async clearTokens() {
|
|
2882
|
+
await secureStorage.removeItem('tokens');
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
static async saveApiKey(key: string) {
|
|
2886
|
+
await secureStorage.setItem('apiKey', key);
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
static async getApiKey(): Promise<string | null> {
|
|
2890
|
+
return secureStorage.getItem('apiKey');
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
\`\`\`
|
|
2894
|
+
|
|
2895
|
+
## When to Use Secure vs Regular Storage
|
|
2896
|
+
|
|
2897
|
+
| Data Type | Use |
|
|
2898
|
+
|-----------|-----|
|
|
2899
|
+
| Auth tokens, refresh tokens | \`createSecureStorage()\` |
|
|
2900
|
+
| API keys, client secrets | \`createSecureStorage()\` |
|
|
2901
|
+
| User preferences, theme | \`storage\` (regular) |
|
|
2902
|
+
| Cache data | \`storage\` (regular) |
|
|
2903
|
+
| Session IDs | \`createSecureStorage()\` |
|
|
2904
|
+
| Language preference | \`storage\` (regular) |
|
|
2905
|
+
|
|
2906
|
+
## Platform Requirements
|
|
2907
|
+
|
|
2908
|
+
- **React Native**: Requires \`react-native-keychain\` (>=9.0.0) and \`react-native-mmkv\` (>=4.0.0)
|
|
2909
|
+
- **Web**: Requires secure context (HTTPS) and IndexedDB support
|
|
2910
|
+
- **Web limitation**: IndexedDB may not be available in some private browsing modes
|
|
2741
2911
|
`
|
|
2742
2912
|
};
|
|
2743
2913
|
|
|
@@ -2769,7 +2939,8 @@ yarn add @idealyst/audio
|
|
|
2769
2939
|
1. **PCM Streaming** \u2014 Audio data is delivered as \`PCMData\` chunks via callbacks, not as files
|
|
2770
2940
|
2. **Audio Session** \u2014 On iOS/Android, configure the audio session category before recording/playback
|
|
2771
2941
|
3. **Audio Profiles** \u2014 Pre-configured \`AudioConfig\` presets: \`speech\`, \`highQuality\`, \`studio\`, \`phone\`
|
|
2772
|
-
4. **Session Presets** \u2014 Pre-configured \`AudioSessionConfig\` presets: \`playback\`, \`record\`, \`voiceChat\`, \`ambient\`, \`default\`
|
|
2942
|
+
4. **Session Presets** \u2014 Pre-configured \`AudioSessionConfig\` presets: \`playback\`, \`record\`, \`voiceChat\`, \`ambient\`, \`default\`, \`backgroundRecord\`
|
|
2943
|
+
5. **Background Recording** \u2014 \`useBackgroundRecorder\` hook for recording that continues when the app is backgrounded (iOS/Android). Requires app-level native entitlements.
|
|
2773
2944
|
|
|
2774
2945
|
## Exports
|
|
2775
2946
|
|
|
@@ -2778,10 +2949,14 @@ import {
|
|
|
2778
2949
|
useRecorder,
|
|
2779
2950
|
usePlayer,
|
|
2780
2951
|
useAudio,
|
|
2952
|
+
useBackgroundRecorder,
|
|
2781
2953
|
AUDIO_PROFILES,
|
|
2782
2954
|
SESSION_PRESETS,
|
|
2783
2955
|
} from '@idealyst/audio';
|
|
2784
|
-
import type {
|
|
2956
|
+
import type {
|
|
2957
|
+
PCMData, AudioConfig, AudioLevel,
|
|
2958
|
+
BackgroundRecorderStatus, BackgroundLifecycleInfo,
|
|
2959
|
+
} from '@idealyst/audio';
|
|
2785
2960
|
\`\`\`
|
|
2786
2961
|
`,
|
|
2787
2962
|
"idealyst://audio/api": `# @idealyst/audio \u2014 API Reference
|
|
@@ -2891,6 +3066,84 @@ interface UseAudioOptions {
|
|
|
2891
3066
|
|
|
2892
3067
|
---
|
|
2893
3068
|
|
|
3069
|
+
### useBackgroundRecorder(options?)
|
|
3070
|
+
|
|
3071
|
+
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).
|
|
3072
|
+
|
|
3073
|
+
> **Requires app-level native configuration** \u2014 see "Background Recording Setup" below.
|
|
3074
|
+
|
|
3075
|
+
\`\`\`typescript
|
|
3076
|
+
interface UseBackgroundRecorderOptions {
|
|
3077
|
+
config?: Partial<AudioConfig>; // Audio config
|
|
3078
|
+
session?: Partial<AudioSessionConfig>; // Session config (default: SESSION_PRESETS.backgroundRecord)
|
|
3079
|
+
autoRequestPermission?: boolean; // Auto-request mic permission on mount
|
|
3080
|
+
levelUpdateInterval?: number; // Level update interval in ms (default: 100)
|
|
3081
|
+
maxBackgroundDuration?: number; // Max background recording time in ms (undefined = no limit)
|
|
3082
|
+
autoConfigureSession?: boolean; // Auto-configure session for background (default: true)
|
|
3083
|
+
onLifecycleEvent?: BackgroundLifecycleCallback; // Lifecycle event callback
|
|
3084
|
+
}
|
|
3085
|
+
\`\`\`
|
|
3086
|
+
|
|
3087
|
+
**Returns \`UseBackgroundRecorderResult\`:**
|
|
3088
|
+
|
|
3089
|
+
All properties from \`useRecorder\`, plus:
|
|
3090
|
+
|
|
3091
|
+
| Property | Type | Description |
|
|
3092
|
+
|----------|------|-------------|
|
|
3093
|
+
| isInBackground | boolean | Whether the app is currently backgrounded |
|
|
3094
|
+
| wasInterrupted | boolean | Whether recording was interrupted (phone call, Siri, etc.) |
|
|
3095
|
+
| backgroundDuration | number | Total time spent recording in background (ms) |
|
|
3096
|
+
| appState | AppStateStatus | Current app state (\`'active' \\| 'background' \\| 'inactive'\`) |
|
|
3097
|
+
|
|
3098
|
+
**Lifecycle events** (via \`onLifecycleEvent\`):
|
|
3099
|
+
|
|
3100
|
+
| Event | When | Extra fields |
|
|
3101
|
+
|-------|------|-------------|
|
|
3102
|
+
| \`'backgrounded'\` | App enters background while recording | \u2014 |
|
|
3103
|
+
| \`'foregrounded'\` | App returns to foreground while recording | \`backgroundDuration\` |
|
|
3104
|
+
| \`'interrupted'\` | OS interrupts recording (phone call, Siri) | \u2014 |
|
|
3105
|
+
| \`'interruptionEnded'\` | OS interruption ends | \`shouldResume\` |
|
|
3106
|
+
| \`'maxDurationReached'\` | Background recording hit \`maxBackgroundDuration\` | \`backgroundDuration\` |
|
|
3107
|
+
| \`'stopped'\` | Recording stopped while in background | \`backgroundDuration\` |
|
|
3108
|
+
|
|
3109
|
+
> **Note:** Interruptions use notify-only \u2014 the hook does NOT auto-resume. The consumer decides via the \`shouldResume\` flag.
|
|
3110
|
+
|
|
3111
|
+
#### Background Recording Setup
|
|
3112
|
+
|
|
3113
|
+
The OS will not allow background recording without app-level entitlements:
|
|
3114
|
+
|
|
3115
|
+
**iOS** \u2014 \`Info.plist\`:
|
|
3116
|
+
\`\`\`xml
|
|
3117
|
+
<key>UIBackgroundModes</key>
|
|
3118
|
+
<array><string>audio</string></array>
|
|
3119
|
+
\`\`\`
|
|
3120
|
+
|
|
3121
|
+
**Android** \u2014 \`AndroidManifest.xml\`:
|
|
3122
|
+
\`\`\`xml
|
|
3123
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
3124
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
|
3125
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
3126
|
+
<service
|
|
3127
|
+
android:name="com.swmansion.audioapi.system.CentralizedForegroundService"
|
|
3128
|
+
android:foregroundServiceType="microphone" />
|
|
3129
|
+
\`\`\`
|
|
3130
|
+
|
|
3131
|
+
**Expo** \u2014 \`app.json\` plugin:
|
|
3132
|
+
\`\`\`json
|
|
3133
|
+
["react-native-audio-api", {
|
|
3134
|
+
"iosBackgroundMode": true,
|
|
3135
|
+
"androidForegroundService": true,
|
|
3136
|
+
"androidFSTypes": ["microphone"],
|
|
3137
|
+
"androidPermissions": [
|
|
3138
|
+
"android.permission.FOREGROUND_SERVICE",
|
|
3139
|
+
"android.permission.FOREGROUND_SERVICE_MICROPHONE",
|
|
3140
|
+
"android.permission.RECORD_AUDIO"
|
|
3141
|
+
]
|
|
3142
|
+
}]
|
|
3143
|
+
\`\`\`
|
|
3144
|
+
|
|
3145
|
+
---
|
|
3146
|
+
|
|
2894
3147
|
## Types
|
|
2895
3148
|
|
|
2896
3149
|
### AudioConfig
|
|
@@ -2952,6 +3205,7 @@ type AudioErrorCode =
|
|
|
2952
3205
|
| 'DEVICE_NOT_FOUND' | 'DEVICE_IN_USE' | 'NOT_SUPPORTED'
|
|
2953
3206
|
| 'SOURCE_NOT_FOUND' | 'FORMAT_NOT_SUPPORTED' | 'DECODE_ERROR' | 'PLAYBACK_ERROR' | 'BUFFER_UNDERRUN'
|
|
2954
3207
|
| 'RECORDING_ERROR'
|
|
3208
|
+
| 'BACKGROUND_NOT_SUPPORTED' | 'BACKGROUND_MAX_DURATION'
|
|
2955
3209
|
| 'INITIALIZATION_FAILED' | 'INVALID_STATE' | 'INVALID_CONFIG' | 'UNKNOWN';
|
|
2956
3210
|
|
|
2957
3211
|
interface AudioError {
|
|
@@ -2980,11 +3234,12 @@ const AUDIO_PROFILES: AudioProfiles = {
|
|
|
2980
3234
|
|
|
2981
3235
|
\`\`\`typescript
|
|
2982
3236
|
const SESSION_PRESETS: SessionPresets = {
|
|
2983
|
-
playback:
|
|
2984
|
-
record:
|
|
2985
|
-
voiceChat:
|
|
2986
|
-
ambient:
|
|
2987
|
-
default:
|
|
3237
|
+
playback: { category: 'playback', mode: 'default' },
|
|
3238
|
+
record: { category: 'record', mode: 'default' },
|
|
3239
|
+
voiceChat: { category: 'playAndRecord', mode: 'voiceChat', categoryOptions: ['allowBluetooth', 'defaultToSpeaker'] },
|
|
3240
|
+
ambient: { category: 'ambient', mode: 'default' },
|
|
3241
|
+
default: { category: 'soloAmbient', mode: 'default' },
|
|
3242
|
+
backgroundRecord: { category: 'playAndRecord', mode: 'spokenAudio', categoryOptions: ['defaultToSpeaker', 'allowBluetooth', 'allowBluetoothA2DP', 'mixWithOthers'] },
|
|
2988
3243
|
};
|
|
2989
3244
|
\`\`\`
|
|
2990
3245
|
`,
|
|
@@ -3157,6 +3412,77 @@ function VoiceChatScreen() {
|
|
|
3157
3412
|
}
|
|
3158
3413
|
\`\`\`
|
|
3159
3414
|
|
|
3415
|
+
## Background Recording for Transcription
|
|
3416
|
+
|
|
3417
|
+
\`\`\`tsx
|
|
3418
|
+
import React, { useEffect } from 'react';
|
|
3419
|
+
import { View, Button, Text } from '@idealyst/components';
|
|
3420
|
+
import { useBackgroundRecorder, AUDIO_PROFILES } from '@idealyst/audio';
|
|
3421
|
+
import type { PCMData, BackgroundLifecycleInfo } from '@idealyst/audio';
|
|
3422
|
+
|
|
3423
|
+
function BackgroundTranscriber() {
|
|
3424
|
+
const recorder = useBackgroundRecorder({
|
|
3425
|
+
config: AUDIO_PROFILES.speech,
|
|
3426
|
+
maxBackgroundDuration: 5 * 60 * 1000, // 5 min max in background
|
|
3427
|
+
onLifecycleEvent: (info: BackgroundLifecycleInfo) => {
|
|
3428
|
+
switch (info.event) {
|
|
3429
|
+
case 'backgrounded':
|
|
3430
|
+
console.log('Recording continues in background');
|
|
3431
|
+
break;
|
|
3432
|
+
case 'foregrounded':
|
|
3433
|
+
console.log(\`Back from background after \${info.backgroundDuration}ms\`);
|
|
3434
|
+
break;
|
|
3435
|
+
case 'interrupted':
|
|
3436
|
+
console.log('Recording interrupted (phone call?)');
|
|
3437
|
+
break;
|
|
3438
|
+
case 'interruptionEnded':
|
|
3439
|
+
if (info.shouldResume) {
|
|
3440
|
+
recorder.resume(); // Consumer decides whether to resume
|
|
3441
|
+
}
|
|
3442
|
+
break;
|
|
3443
|
+
case 'maxDurationReached':
|
|
3444
|
+
console.log('Max background duration reached');
|
|
3445
|
+
break;
|
|
3446
|
+
}
|
|
3447
|
+
},
|
|
3448
|
+
});
|
|
3449
|
+
|
|
3450
|
+
// Stream PCM chunks to your speech-to-text service
|
|
3451
|
+
useEffect(() => {
|
|
3452
|
+
const unsub = recorder.subscribeToData((pcm: PCMData) => {
|
|
3453
|
+
// Send to STT API (e.g., Whisper, Deepgram)
|
|
3454
|
+
sendToTranscriptionService(pcm.toBase64());
|
|
3455
|
+
});
|
|
3456
|
+
return unsub;
|
|
3457
|
+
}, [recorder.subscribeToData]);
|
|
3458
|
+
|
|
3459
|
+
const handleToggle = async () => {
|
|
3460
|
+
if (recorder.isRecording) {
|
|
3461
|
+
await recorder.stop();
|
|
3462
|
+
} else {
|
|
3463
|
+
await recorder.start();
|
|
3464
|
+
}
|
|
3465
|
+
};
|
|
3466
|
+
|
|
3467
|
+
return (
|
|
3468
|
+
<View padding="md" gap="md">
|
|
3469
|
+
<Button
|
|
3470
|
+
onPress={handleToggle}
|
|
3471
|
+
intent={recorder.isRecording ? 'error' : 'primary'}
|
|
3472
|
+
>
|
|
3473
|
+
{recorder.isRecording ? 'Stop' : 'Record'}
|
|
3474
|
+
</Button>
|
|
3475
|
+
<Text>Duration: {Math.round(recorder.duration / 1000)}s</Text>
|
|
3476
|
+
{recorder.isInBackground && <Text>Recording in background...</Text>}
|
|
3477
|
+
{recorder.wasInterrupted && <Text>Recording was interrupted</Text>}
|
|
3478
|
+
<Text>Background time: {Math.round(recorder.backgroundDuration / 1000)}s</Text>
|
|
3479
|
+
</View>
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
\`\`\`
|
|
3483
|
+
|
|
3484
|
+
> **Important:** Background recording requires native entitlements. See the \`useBackgroundRecorder\` API docs for iOS, Android, and Expo setup instructions.
|
|
3485
|
+
|
|
3160
3486
|
## Audio Level Visualization
|
|
3161
3487
|
|
|
3162
3488
|
\`\`\`tsx
|
|
@@ -6650,146 +6976,1662 @@ function SalesOverview() {
|
|
|
6650
6976
|
}
|
|
6651
6977
|
\`\`\`
|
|
6652
6978
|
|
|
6653
|
-
## Multi-Series Line Chart
|
|
6979
|
+
## Multi-Series Line Chart
|
|
6980
|
+
|
|
6981
|
+
\`\`\`tsx
|
|
6982
|
+
import React from 'react';
|
|
6983
|
+
import { LineChart } from '@idealyst/charts';
|
|
6984
|
+
import type { ChartDataSeries } from '@idealyst/charts';
|
|
6985
|
+
|
|
6986
|
+
// Each series has: id, name, data, color? \u2014 NO 'label' property
|
|
6987
|
+
const series: ChartDataSeries[] = [
|
|
6988
|
+
{
|
|
6989
|
+
id: 'product-a',
|
|
6990
|
+
name: 'Product A', // Use 'name' \u2014 NOT 'label'
|
|
6991
|
+
data: [
|
|
6992
|
+
{ x: 'Q1', y: 120 },
|
|
6993
|
+
{ x: 'Q2', y: 150 },
|
|
6994
|
+
{ x: 'Q3', y: 180 },
|
|
6995
|
+
{ x: 'Q4', y: 210 },
|
|
6996
|
+
],
|
|
6997
|
+
color: '#2196F3',
|
|
6998
|
+
},
|
|
6999
|
+
{
|
|
7000
|
+
id: 'product-b',
|
|
7001
|
+
name: 'Product B', // Use 'name' \u2014 NOT 'label'
|
|
7002
|
+
data: [
|
|
7003
|
+
{ x: 'Q1', y: 80 },
|
|
7004
|
+
{ x: 'Q2', y: 110 },
|
|
7005
|
+
{ x: 'Q3', y: 95 },
|
|
7006
|
+
{ x: 'Q4', y: 140 },
|
|
7007
|
+
],
|
|
7008
|
+
color: '#FF9800',
|
|
7009
|
+
},
|
|
7010
|
+
];
|
|
7011
|
+
|
|
7012
|
+
function ComparisonChart() {
|
|
7013
|
+
return (
|
|
7014
|
+
<LineChart
|
|
7015
|
+
data={series}
|
|
7016
|
+
height={350}
|
|
7017
|
+
curve="monotone"
|
|
7018
|
+
showDots
|
|
7019
|
+
animate
|
|
7020
|
+
/>
|
|
7021
|
+
);
|
|
7022
|
+
}
|
|
7023
|
+
\`\`\`
|
|
7024
|
+
|
|
7025
|
+
## Bar Chart
|
|
7026
|
+
|
|
7027
|
+
\`\`\`tsx
|
|
7028
|
+
import React from 'react';
|
|
7029
|
+
import { View, Text } from '@idealyst/components';
|
|
7030
|
+
import { BarChart } from '@idealyst/charts';
|
|
7031
|
+
|
|
7032
|
+
const categories = [
|
|
7033
|
+
{ x: 'Electronics', y: 45 },
|
|
7034
|
+
{ x: 'Clothing', y: 32 },
|
|
7035
|
+
{ x: 'Books', y: 18 },
|
|
7036
|
+
{ x: 'Food', y: 56 },
|
|
7037
|
+
{ x: 'Sports', y: 28 },
|
|
7038
|
+
];
|
|
7039
|
+
|
|
7040
|
+
function CategoryBreakdown() {
|
|
7041
|
+
return (
|
|
7042
|
+
<View padding="md" gap="md">
|
|
7043
|
+
<Text typography="h6" weight="bold">Sales by Category</Text>
|
|
7044
|
+
<BarChart
|
|
7045
|
+
data={[{ id: 'units', name: 'Units Sold', data: categories }]}
|
|
7046
|
+
height={300}
|
|
7047
|
+
barRadius={4}
|
|
7048
|
+
animate
|
|
7049
|
+
yAxis={{ tickFormat: (value: number | string | Date) => \`\${value} units\` }}
|
|
7050
|
+
/>
|
|
7051
|
+
</View>
|
|
7052
|
+
);
|
|
7053
|
+
}
|
|
7054
|
+
\`\`\`
|
|
7055
|
+
|
|
7056
|
+
## Stacked Bar Chart
|
|
7057
|
+
|
|
7058
|
+
\`\`\`tsx
|
|
7059
|
+
import React from 'react';
|
|
7060
|
+
import { BarChart } from '@idealyst/charts';
|
|
7061
|
+
|
|
7062
|
+
function StackedBarExample() {
|
|
7063
|
+
return (
|
|
7064
|
+
<BarChart
|
|
7065
|
+
data={[
|
|
7066
|
+
{
|
|
7067
|
+
id: 'online',
|
|
7068
|
+
name: 'Online',
|
|
7069
|
+
data: [
|
|
7070
|
+
{ x: 'Q1', y: 100 },
|
|
7071
|
+
{ x: 'Q2', y: 120 },
|
|
7072
|
+
{ x: 'Q3', y: 90 },
|
|
7073
|
+
],
|
|
7074
|
+
color: '#4CAF50',
|
|
7075
|
+
},
|
|
7076
|
+
{
|
|
7077
|
+
id: 'in-store',
|
|
7078
|
+
name: 'In-Store',
|
|
7079
|
+
data: [
|
|
7080
|
+
{ x: 'Q1', y: 60 },
|
|
7081
|
+
{ x: 'Q2', y: 80 },
|
|
7082
|
+
{ x: 'Q3', y: 70 },
|
|
7083
|
+
],
|
|
7084
|
+
color: '#2196F3',
|
|
7085
|
+
},
|
|
7086
|
+
]}
|
|
7087
|
+
height={300}
|
|
7088
|
+
stacked
|
|
7089
|
+
animate
|
|
7090
|
+
/>
|
|
7091
|
+
);
|
|
7092
|
+
}
|
|
7093
|
+
\`\`\`
|
|
7094
|
+
|
|
7095
|
+
## Horizontal Bar Chart
|
|
7096
|
+
|
|
7097
|
+
\`\`\`tsx
|
|
7098
|
+
import React from 'react';
|
|
7099
|
+
import { BarChart } from '@idealyst/charts';
|
|
7100
|
+
|
|
7101
|
+
function HorizontalBarExample() {
|
|
7102
|
+
const data = [
|
|
7103
|
+
{ x: 'React', y: 85 },
|
|
7104
|
+
{ x: 'Vue', y: 62 },
|
|
7105
|
+
{ x: 'Angular', y: 45 },
|
|
7106
|
+
{ x: 'Svelte', y: 38 },
|
|
7107
|
+
];
|
|
7108
|
+
|
|
7109
|
+
return (
|
|
7110
|
+
<BarChart
|
|
7111
|
+
data={[{ id: 'popularity', name: 'Popularity', data }]}
|
|
7112
|
+
height={250}
|
|
7113
|
+
orientation="horizontal"
|
|
7114
|
+
animate
|
|
7115
|
+
/>
|
|
7116
|
+
);
|
|
7117
|
+
}
|
|
7118
|
+
\`\`\`
|
|
7119
|
+
`
|
|
7120
|
+
};
|
|
7121
|
+
|
|
7122
|
+
// src/data/clipboard-guides.ts
|
|
7123
|
+
var clipboardGuides = {
|
|
7124
|
+
"idealyst://clipboard/overview": `# @idealyst/clipboard Overview
|
|
7125
|
+
|
|
7126
|
+
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.
|
|
7127
|
+
|
|
7128
|
+
## Features
|
|
7129
|
+
|
|
7130
|
+
- **Cross-Platform Clipboard** - Copy and paste text on React Native and Web
|
|
7131
|
+
- **Simple API** - Async/await based with consistent interface
|
|
7132
|
+
- **React Native** - Uses @react-native-clipboard/clipboard
|
|
7133
|
+
- **Web** - Uses navigator.clipboard API
|
|
7134
|
+
- **OTP Auto-Fill (Android)** - Automatically reads OTP codes from SMS via SMS Retriever API (no permissions needed)
|
|
7135
|
+
- **OTP Auto-Fill (iOS)** - Provides TextInput props for native iOS keyboard OTP suggestion
|
|
7136
|
+
- **TypeScript** - Full type safety and IntelliSense support
|
|
7137
|
+
|
|
7138
|
+
## Installation
|
|
7139
|
+
|
|
7140
|
+
\`\`\`bash
|
|
7141
|
+
yarn add @idealyst/clipboard
|
|
7142
|
+
|
|
7143
|
+
# React Native also needs:
|
|
7144
|
+
yarn add @react-native-clipboard/clipboard
|
|
7145
|
+
cd ios && pod install
|
|
7146
|
+
|
|
7147
|
+
# For OTP autofill on Android (optional):
|
|
7148
|
+
yarn add react-native-otp-verify
|
|
7149
|
+
\`\`\`
|
|
7150
|
+
|
|
7151
|
+
## Quick Start
|
|
7152
|
+
|
|
7153
|
+
\`\`\`tsx
|
|
7154
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7155
|
+
|
|
7156
|
+
// Copy text
|
|
7157
|
+
await clipboard.copy('Hello, world!');
|
|
7158
|
+
|
|
7159
|
+
// Paste text
|
|
7160
|
+
const text = await clipboard.paste();
|
|
7161
|
+
|
|
7162
|
+
// Check if clipboard has text
|
|
7163
|
+
const hasText = await clipboard.hasText();
|
|
7164
|
+
\`\`\`
|
|
7165
|
+
|
|
7166
|
+
## OTP Auto-Fill Quick Start
|
|
7167
|
+
|
|
7168
|
+
\`\`\`tsx
|
|
7169
|
+
import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
7170
|
+
import { TextInput } from 'react-native';
|
|
7171
|
+
|
|
7172
|
+
function OTPScreen() {
|
|
7173
|
+
const { code, startListening, hash } = useOTPAutoFill({
|
|
7174
|
+
codeLength: 6,
|
|
7175
|
+
onCodeReceived: (otp) => verifyOTP(otp),
|
|
7176
|
+
});
|
|
7177
|
+
|
|
7178
|
+
useEffect(() => {
|
|
7179
|
+
startListening();
|
|
7180
|
+
}, []);
|
|
7181
|
+
|
|
7182
|
+
return (
|
|
7183
|
+
<TextInput
|
|
7184
|
+
value={code ?? ''}
|
|
7185
|
+
{...OTP_INPUT_PROPS}
|
|
7186
|
+
/>
|
|
7187
|
+
);
|
|
7188
|
+
}
|
|
7189
|
+
\`\`\`
|
|
7190
|
+
|
|
7191
|
+
## Import Options
|
|
7192
|
+
|
|
7193
|
+
\`\`\`tsx
|
|
7194
|
+
// Named import (recommended)
|
|
7195
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7196
|
+
|
|
7197
|
+
// Default import
|
|
7198
|
+
import clipboard from '@idealyst/clipboard';
|
|
7199
|
+
|
|
7200
|
+
// OTP hook and helpers
|
|
7201
|
+
import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
7202
|
+
\`\`\`
|
|
7203
|
+
|
|
7204
|
+
## Platform Details
|
|
7205
|
+
|
|
7206
|
+
- **React Native**: Uses \`@react-native-clipboard/clipboard\` for clipboard operations
|
|
7207
|
+
- **Web**: Uses \`navigator.clipboard\` API (requires secure context / HTTPS)
|
|
7208
|
+
- **OTP (Android)**: Uses SMS Retriever API via \`react-native-otp-verify\` \u2014 zero permissions
|
|
7209
|
+
- **OTP (iOS)**: Native keyboard autofill via \`textContentType="oneTimeCode"\`
|
|
7210
|
+
- **OTP (Web)**: No-op \u2014 returns null values and noop functions
|
|
7211
|
+
`,
|
|
7212
|
+
"idealyst://clipboard/api": `# Clipboard API Reference
|
|
7213
|
+
|
|
7214
|
+
Complete API reference for @idealyst/clipboard.
|
|
7215
|
+
|
|
7216
|
+
## clipboard.copy
|
|
7217
|
+
|
|
7218
|
+
Copy text to the system clipboard.
|
|
7219
|
+
|
|
7220
|
+
\`\`\`tsx
|
|
7221
|
+
await clipboard.copy(text: string): Promise<void>
|
|
7222
|
+
|
|
7223
|
+
// Examples
|
|
7224
|
+
await clipboard.copy('Hello, world!');
|
|
7225
|
+
await clipboard.copy(inviteCode);
|
|
7226
|
+
await clipboard.copy(JSON.stringify(data));
|
|
7227
|
+
\`\`\`
|
|
7228
|
+
|
|
7229
|
+
## clipboard.paste
|
|
7230
|
+
|
|
7231
|
+
Read text from the system clipboard.
|
|
7232
|
+
|
|
7233
|
+
\`\`\`tsx
|
|
7234
|
+
await clipboard.paste(): Promise<string>
|
|
7235
|
+
|
|
7236
|
+
// Examples
|
|
7237
|
+
const text = await clipboard.paste();
|
|
7238
|
+
const url = await clipboard.paste();
|
|
7239
|
+
\`\`\`
|
|
7240
|
+
|
|
7241
|
+
## clipboard.hasText
|
|
7242
|
+
|
|
7243
|
+
Check if the clipboard contains text content.
|
|
7244
|
+
|
|
7245
|
+
\`\`\`tsx
|
|
7246
|
+
await clipboard.hasText(): Promise<boolean>
|
|
7247
|
+
|
|
7248
|
+
// Example
|
|
7249
|
+
const canPaste = await clipboard.hasText();
|
|
7250
|
+
if (canPaste) {
|
|
7251
|
+
const text = await clipboard.paste();
|
|
7252
|
+
}
|
|
7253
|
+
\`\`\`
|
|
7254
|
+
|
|
7255
|
+
## clipboard.addListener
|
|
7256
|
+
|
|
7257
|
+
Listen for copy events (triggered when \`clipboard.copy()\` is called).
|
|
7258
|
+
|
|
7259
|
+
\`\`\`tsx
|
|
7260
|
+
const unsubscribe = clipboard.addListener((content: string) => {
|
|
7261
|
+
console.log('Copied:', content);
|
|
7262
|
+
});
|
|
7263
|
+
|
|
7264
|
+
// Later, unsubscribe
|
|
7265
|
+
unsubscribe();
|
|
7266
|
+
\`\`\`
|
|
7267
|
+
|
|
7268
|
+
---
|
|
7269
|
+
|
|
7270
|
+
## useOTPAutoFill
|
|
7271
|
+
|
|
7272
|
+
React hook for automatic OTP code detection from SMS on mobile.
|
|
7273
|
+
|
|
7274
|
+
**Android**: Uses SMS Retriever API to auto-read OTP from incoming SMS (no permissions required). SMS must include your app hash.
|
|
7275
|
+
|
|
7276
|
+
**iOS**: OTP is handled natively by the iOS keyboard. Use \`OTP_INPUT_PROPS\` on your TextInput to enable it.
|
|
7277
|
+
|
|
7278
|
+
**Web**: Returns no-op values.
|
|
7279
|
+
|
|
7280
|
+
\`\`\`tsx
|
|
7281
|
+
const {
|
|
7282
|
+
code, // string | null - received OTP code (Android only)
|
|
7283
|
+
startListening, // () => void - begin listening for SMS (Android only)
|
|
7284
|
+
stopListening, // () => void - stop listening (Android only)
|
|
7285
|
+
hash, // string | null - app hash for SMS body (Android only)
|
|
7286
|
+
} = useOTPAutoFill(options?: {
|
|
7287
|
+
codeLength?: number; // default: 6
|
|
7288
|
+
onCodeReceived?: (code: string) => void;
|
|
7289
|
+
});
|
|
7290
|
+
\`\`\`
|
|
7291
|
+
|
|
7292
|
+
### Options
|
|
7293
|
+
|
|
7294
|
+
| Option | Type | Default | Description |
|
|
7295
|
+
|--------|------|---------|-------------|
|
|
7296
|
+
| codeLength | number | 6 | Expected digit count of OTP code |
|
|
7297
|
+
| onCodeReceived | (code: string) => void | \u2014 | Callback when OTP is detected |
|
|
7298
|
+
|
|
7299
|
+
### Return Value
|
|
7300
|
+
|
|
7301
|
+
| Property | Type | Description |
|
|
7302
|
+
|----------|------|-------------|
|
|
7303
|
+
| code | string \\| null | Detected OTP code (Android). Null on iOS/web. |
|
|
7304
|
+
| startListening | () => void | Start SMS listener (Android). No-op on iOS/web. |
|
|
7305
|
+
| stopListening | () => void | Stop SMS listener (Android). No-op on iOS/web. |
|
|
7306
|
+
| hash | string \\| null | App hash for SMS Retriever (Android). Null on iOS/web. |
|
|
7307
|
+
|
|
7308
|
+
### Android SMS Format
|
|
7309
|
+
|
|
7310
|
+
For the SMS Retriever API to detect the message, the SMS must:
|
|
7311
|
+
1. Start with \`<#>\`
|
|
7312
|
+
2. Contain the OTP code as a sequence of digits
|
|
7313
|
+
3. End with your app's 11-character hash (available via \`hash\`)
|
|
7314
|
+
|
|
7315
|
+
Example SMS:
|
|
7316
|
+
\`\`\`
|
|
7317
|
+
<#> Your verification code is: 123456
|
|
7318
|
+
FA+9qCX9VSu
|
|
7319
|
+
\`\`\`
|
|
7320
|
+
|
|
7321
|
+
---
|
|
7322
|
+
|
|
7323
|
+
## OTP_INPUT_PROPS
|
|
7324
|
+
|
|
7325
|
+
Constant with TextInput props to enable native OTP keyboard autofill.
|
|
7326
|
+
|
|
7327
|
+
\`\`\`tsx
|
|
7328
|
+
import { OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
7329
|
+
|
|
7330
|
+
// Value:
|
|
7331
|
+
// {
|
|
7332
|
+
// textContentType: 'oneTimeCode',
|
|
7333
|
+
// autoComplete: 'sms-otp',
|
|
7334
|
+
// }
|
|
7335
|
+
|
|
7336
|
+
<TextInput
|
|
7337
|
+
{...OTP_INPUT_PROPS}
|
|
7338
|
+
value={code}
|
|
7339
|
+
onChangeText={setCode}
|
|
7340
|
+
/>
|
|
7341
|
+
\`\`\`
|
|
7342
|
+
|
|
7343
|
+
- **iOS**: Enables the keyboard to suggest OTP codes from received SMS (iOS 12+)
|
|
7344
|
+
- **Android**: Maps to the correct \`autoComplete\` value
|
|
7345
|
+
- **Web**: Harmless \u2014 ignored by web TextInput
|
|
7346
|
+
`,
|
|
7347
|
+
"idealyst://clipboard/examples": `# Clipboard Examples
|
|
7348
|
+
|
|
7349
|
+
Complete code examples for common @idealyst/clipboard patterns.
|
|
7350
|
+
|
|
7351
|
+
## Copy to Clipboard with Feedback
|
|
7352
|
+
|
|
7353
|
+
\`\`\`tsx
|
|
7354
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7355
|
+
import { useState, useCallback } from 'react';
|
|
7356
|
+
|
|
7357
|
+
function CopyButton({ text }: { text: string }) {
|
|
7358
|
+
const [copied, setCopied] = useState(false);
|
|
7359
|
+
|
|
7360
|
+
const handleCopy = useCallback(async () => {
|
|
7361
|
+
await clipboard.copy(text);
|
|
7362
|
+
setCopied(true);
|
|
7363
|
+
setTimeout(() => setCopied(false), 2000);
|
|
7364
|
+
}, [text]);
|
|
7365
|
+
|
|
7366
|
+
return (
|
|
7367
|
+
<Button
|
|
7368
|
+
label={copied ? 'Copied!' : 'Copy'}
|
|
7369
|
+
intent={copied ? 'positive' : 'neutral'}
|
|
7370
|
+
onPress={handleCopy}
|
|
7371
|
+
/>
|
|
7372
|
+
);
|
|
7373
|
+
}
|
|
7374
|
+
\`\`\`
|
|
7375
|
+
|
|
7376
|
+
## Share / Copy Link
|
|
7377
|
+
|
|
7378
|
+
\`\`\`tsx
|
|
7379
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7380
|
+
|
|
7381
|
+
async function copyShareLink(itemId: string) {
|
|
7382
|
+
const url = \`https://myapp.com/items/\${itemId}\`;
|
|
7383
|
+
await clipboard.copy(url);
|
|
7384
|
+
}
|
|
7385
|
+
\`\`\`
|
|
7386
|
+
|
|
7387
|
+
## Paste from Clipboard
|
|
7388
|
+
|
|
7389
|
+
\`\`\`tsx
|
|
7390
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7391
|
+
|
|
7392
|
+
function PasteInput() {
|
|
7393
|
+
const [value, setValue] = useState('');
|
|
7394
|
+
|
|
7395
|
+
const handlePaste = useCallback(async () => {
|
|
7396
|
+
const hasText = await clipboard.hasText();
|
|
7397
|
+
if (hasText) {
|
|
7398
|
+
const text = await clipboard.paste();
|
|
7399
|
+
setValue(text);
|
|
7400
|
+
}
|
|
7401
|
+
}, []);
|
|
7402
|
+
|
|
7403
|
+
return (
|
|
7404
|
+
<View>
|
|
7405
|
+
<Input value={value} onChangeText={setValue} placeholder="Enter or paste text" />
|
|
7406
|
+
<Button label="Paste" onPress={handlePaste} iconName="content-paste" />
|
|
7407
|
+
</View>
|
|
7408
|
+
);
|
|
7409
|
+
}
|
|
7410
|
+
\`\`\`
|
|
7411
|
+
|
|
7412
|
+
## useClipboard Hook
|
|
7413
|
+
|
|
7414
|
+
\`\`\`tsx
|
|
7415
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7416
|
+
import { useState, useCallback } from 'react';
|
|
7417
|
+
|
|
7418
|
+
export function useClipboard() {
|
|
7419
|
+
const [copiedText, setCopiedText] = useState<string | null>(null);
|
|
7420
|
+
|
|
7421
|
+
const copy = useCallback(async (text: string) => {
|
|
7422
|
+
await clipboard.copy(text);
|
|
7423
|
+
setCopiedText(text);
|
|
7424
|
+
}, []);
|
|
7425
|
+
|
|
7426
|
+
const paste = useCallback(async () => {
|
|
7427
|
+
return clipboard.paste();
|
|
7428
|
+
}, []);
|
|
7429
|
+
|
|
7430
|
+
const reset = useCallback(() => {
|
|
7431
|
+
setCopiedText(null);
|
|
7432
|
+
}, []);
|
|
7433
|
+
|
|
7434
|
+
return { copy, paste, copiedText, reset };
|
|
7435
|
+
}
|
|
7436
|
+
\`\`\`
|
|
7437
|
+
|
|
7438
|
+
## OTP Verification Screen
|
|
7439
|
+
|
|
7440
|
+
\`\`\`tsx
|
|
7441
|
+
import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
7442
|
+
import { useEffect, useState } from 'react';
|
|
7443
|
+
import { TextInput, Platform } from 'react-native';
|
|
7444
|
+
|
|
7445
|
+
function OTPVerificationScreen({ phoneNumber, onVerify }: {
|
|
7446
|
+
phoneNumber: string;
|
|
7447
|
+
onVerify: (code: string) => void;
|
|
7448
|
+
}) {
|
|
7449
|
+
const [code, setCode] = useState('');
|
|
7450
|
+
|
|
7451
|
+
const otp = useOTPAutoFill({
|
|
7452
|
+
codeLength: 6,
|
|
7453
|
+
onCodeReceived: (receivedCode) => {
|
|
7454
|
+
setCode(receivedCode);
|
|
7455
|
+
onVerify(receivedCode);
|
|
7456
|
+
},
|
|
7457
|
+
});
|
|
7458
|
+
|
|
7459
|
+
// Start listening when screen mounts
|
|
7460
|
+
useEffect(() => {
|
|
7461
|
+
otp.startListening();
|
|
7462
|
+
return () => otp.stopListening();
|
|
7463
|
+
}, []);
|
|
7464
|
+
|
|
7465
|
+
// Auto-fill from hook on Android
|
|
7466
|
+
useEffect(() => {
|
|
7467
|
+
if (otp.code) {
|
|
7468
|
+
setCode(otp.code);
|
|
7469
|
+
}
|
|
7470
|
+
}, [otp.code]);
|
|
7471
|
+
|
|
7472
|
+
return (
|
|
7473
|
+
<View>
|
|
7474
|
+
<Text>Enter the code sent to {phoneNumber}</Text>
|
|
7475
|
+
|
|
7476
|
+
<TextInput
|
|
7477
|
+
value={code}
|
|
7478
|
+
onChangeText={(text) => {
|
|
7479
|
+
setCode(text);
|
|
7480
|
+
if (text.length === 6) onVerify(text);
|
|
7481
|
+
}}
|
|
7482
|
+
keyboardType="number-pad"
|
|
7483
|
+
maxLength={6}
|
|
7484
|
+
{...OTP_INPUT_PROPS}
|
|
7485
|
+
/>
|
|
7486
|
+
|
|
7487
|
+
{Platform.OS === 'android' && otp.hash && (
|
|
7488
|
+
<Text style={{ fontSize: 10, color: '#999' }}>
|
|
7489
|
+
App hash (for SMS setup): {otp.hash}
|
|
7490
|
+
</Text>
|
|
7491
|
+
)}
|
|
7492
|
+
</View>
|
|
7493
|
+
);
|
|
7494
|
+
}
|
|
7495
|
+
\`\`\`
|
|
7496
|
+
|
|
7497
|
+
## Copy Invite Code
|
|
7498
|
+
|
|
7499
|
+
\`\`\`tsx
|
|
7500
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7501
|
+
|
|
7502
|
+
function InviteCard({ code }: { code: string }) {
|
|
7503
|
+
const [copied, setCopied] = useState(false);
|
|
7504
|
+
|
|
7505
|
+
return (
|
|
7506
|
+
<Card>
|
|
7507
|
+
<Text>Your invite code</Text>
|
|
7508
|
+
<Text variant="headline">{code}</Text>
|
|
7509
|
+
<Button
|
|
7510
|
+
label={copied ? 'Copied!' : 'Copy Code'}
|
|
7511
|
+
iconName={copied ? 'check' : 'content-copy'}
|
|
7512
|
+
onPress={async () => {
|
|
7513
|
+
await clipboard.copy(code);
|
|
7514
|
+
setCopied(true);
|
|
7515
|
+
setTimeout(() => setCopied(false), 2000);
|
|
7516
|
+
}}
|
|
7517
|
+
/>
|
|
7518
|
+
</Card>
|
|
7519
|
+
);
|
|
7520
|
+
}
|
|
7521
|
+
\`\`\`
|
|
7522
|
+
|
|
7523
|
+
## Error Handling
|
|
7524
|
+
|
|
7525
|
+
\`\`\`tsx
|
|
7526
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7527
|
+
|
|
7528
|
+
async function safeCopy(text: string): Promise<boolean> {
|
|
7529
|
+
try {
|
|
7530
|
+
await clipboard.copy(text);
|
|
7531
|
+
return true;
|
|
7532
|
+
} catch (error) {
|
|
7533
|
+
console.error('Clipboard copy failed:', error);
|
|
7534
|
+
// On web, this can fail if not in a secure context or without user gesture
|
|
7535
|
+
return false;
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
|
|
7539
|
+
async function safePaste(): Promise<string | null> {
|
|
7540
|
+
try {
|
|
7541
|
+
const hasText = await clipboard.hasText();
|
|
7542
|
+
if (!hasText) return null;
|
|
7543
|
+
return await clipboard.paste();
|
|
7544
|
+
} catch (error) {
|
|
7545
|
+
console.error('Clipboard paste failed:', error);
|
|
7546
|
+
// Browser may deny permission
|
|
7547
|
+
return null;
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
\`\`\`
|
|
7551
|
+
|
|
7552
|
+
## Best Practices
|
|
7553
|
+
|
|
7554
|
+
1. **Always use try-catch** \u2014 Clipboard operations can fail (permissions, secure context)
|
|
7555
|
+
2. **Provide visual feedback** \u2014 Show a "Copied!" confirmation after copying
|
|
7556
|
+
3. **Use OTP_INPUT_PROPS** \u2014 Always spread these on OTP text inputs for cross-platform autofill
|
|
7557
|
+
4. **Start OTP listener on mount** \u2014 Call \`startListening()\` in useEffect, clean up with \`stopListening()\`
|
|
7558
|
+
5. **Log the Android hash** \u2014 During development, log \`hash\` to configure your SMS gateway
|
|
7559
|
+
6. **Graceful degradation** \u2014 OTP features degrade gracefully if native modules aren't installed
|
|
7560
|
+
7. **Secure context (web)** \u2014 Clipboard API requires HTTPS on web
|
|
7561
|
+
`
|
|
7562
|
+
};
|
|
7563
|
+
|
|
7564
|
+
// src/data/biometrics-guides.ts
|
|
7565
|
+
var biometricsGuides = {
|
|
7566
|
+
"idealyst://biometrics/overview": `# @idealyst/biometrics Overview
|
|
7567
|
+
|
|
7568
|
+
Cross-platform biometric authentication and passkeys (WebAuthn/FIDO2) for React and React Native applications.
|
|
7569
|
+
|
|
7570
|
+
## Features
|
|
7571
|
+
|
|
7572
|
+
- **Local Biometric Auth** \u2014 FaceID, TouchID, fingerprint, iris to gate access
|
|
7573
|
+
- **Passkeys (WebAuthn/FIDO2)** \u2014 Passwordless login with cryptographic credentials
|
|
7574
|
+
- **Cross-Platform** \u2014 Single API for React Native (iOS/Android) and Web
|
|
7575
|
+
- **React Native** \u2014 Uses expo-local-authentication for biometrics, react-native-passkeys for passkeys
|
|
7576
|
+
- **Web** \u2014 Uses WebAuthn API for both biometrics and passkeys
|
|
7577
|
+
- **Graceful Degradation** \u2014 Falls back cleanly when native modules aren't installed
|
|
7578
|
+
- **TypeScript** \u2014 Full type safety and IntelliSense support
|
|
7579
|
+
|
|
7580
|
+
## Installation
|
|
7581
|
+
|
|
7582
|
+
\`\`\`bash
|
|
7583
|
+
yarn add @idealyst/biometrics
|
|
7584
|
+
|
|
7585
|
+
# React Native \u2014 biometric auth:
|
|
7586
|
+
yarn add expo-local-authentication
|
|
7587
|
+
cd ios && pod install
|
|
7588
|
+
|
|
7589
|
+
# React Native \u2014 passkeys (optional):
|
|
7590
|
+
yarn add react-native-passkeys
|
|
7591
|
+
cd ios && pod install
|
|
7592
|
+
\`\`\`
|
|
7593
|
+
|
|
7594
|
+
## Quick Start \u2014 Biometric Auth
|
|
7595
|
+
|
|
7596
|
+
\`\`\`tsx
|
|
7597
|
+
import { isBiometricAvailable, authenticate } from '@idealyst/biometrics';
|
|
7598
|
+
|
|
7599
|
+
// Check availability
|
|
7600
|
+
const available = await isBiometricAvailable();
|
|
7601
|
+
|
|
7602
|
+
// Prompt user
|
|
7603
|
+
if (available) {
|
|
7604
|
+
const result = await authenticate({
|
|
7605
|
+
promptMessage: 'Verify your identity',
|
|
7606
|
+
});
|
|
7607
|
+
|
|
7608
|
+
if (result.success) {
|
|
7609
|
+
// Authenticated!
|
|
7610
|
+
} else {
|
|
7611
|
+
console.log(result.error, result.message);
|
|
7612
|
+
}
|
|
7613
|
+
}
|
|
7614
|
+
\`\`\`
|
|
7615
|
+
|
|
7616
|
+
## Quick Start \u2014 Passkeys
|
|
7617
|
+
|
|
7618
|
+
\`\`\`tsx
|
|
7619
|
+
import { isPasskeySupported, createPasskey, getPasskey } from '@idealyst/biometrics';
|
|
7620
|
+
|
|
7621
|
+
// Check support
|
|
7622
|
+
const supported = await isPasskeySupported();
|
|
7623
|
+
|
|
7624
|
+
// Register a new passkey
|
|
7625
|
+
const credential = await createPasskey({
|
|
7626
|
+
challenge: serverChallenge,
|
|
7627
|
+
rp: { id: 'example.com', name: 'My App' },
|
|
7628
|
+
user: { id: userId, name: email, displayName: name },
|
|
7629
|
+
});
|
|
7630
|
+
// Send credential to server for verification
|
|
7631
|
+
|
|
7632
|
+
// Sign in with passkey
|
|
7633
|
+
const assertion = await getPasskey({
|
|
7634
|
+
challenge: serverChallenge,
|
|
7635
|
+
rpId: 'example.com',
|
|
7636
|
+
});
|
|
7637
|
+
// Send assertion to server for verification
|
|
7638
|
+
\`\`\`
|
|
7639
|
+
|
|
7640
|
+
## Platform Details
|
|
7641
|
+
|
|
7642
|
+
- **React Native (biometrics)**: Uses \`expo-local-authentication\` \u2014 FaceID, TouchID, fingerprint, iris
|
|
7643
|
+
- **React Native (passkeys)**: Uses \`react-native-passkeys\` \u2014 system passkey UI on iOS 16+ and Android 9+
|
|
7644
|
+
- **Web (biometrics)**: Uses WebAuthn with \`userVerification: 'required'\` to trigger platform authenticator
|
|
7645
|
+
- **Web (passkeys)**: Uses \`navigator.credentials.create/get\` with the PublicKey API
|
|
7646
|
+
`,
|
|
7647
|
+
"idealyst://biometrics/api": `# Biometrics API Reference
|
|
7648
|
+
|
|
7649
|
+
Complete API reference for @idealyst/biometrics.
|
|
7650
|
+
|
|
7651
|
+
---
|
|
7652
|
+
|
|
7653
|
+
## Biometric Authentication Functions
|
|
7654
|
+
|
|
7655
|
+
### isBiometricAvailable
|
|
7656
|
+
|
|
7657
|
+
Check whether biometric auth hardware is available and enrolled.
|
|
7658
|
+
|
|
7659
|
+
\`\`\`tsx
|
|
7660
|
+
await isBiometricAvailable(): Promise<boolean>
|
|
7661
|
+
|
|
7662
|
+
// Native: checks hardware + enrollment via expo-local-authentication
|
|
7663
|
+
// Web: checks PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
|
|
7664
|
+
\`\`\`
|
|
7665
|
+
|
|
7666
|
+
### getBiometricTypes
|
|
7667
|
+
|
|
7668
|
+
Return the biometric types available on this device.
|
|
7669
|
+
|
|
7670
|
+
\`\`\`tsx
|
|
7671
|
+
await getBiometricTypes(): Promise<BiometricType[]>
|
|
7672
|
+
|
|
7673
|
+
type BiometricType = 'fingerprint' | 'facial_recognition' | 'iris';
|
|
7674
|
+
|
|
7675
|
+
// Native: returns specific types (fingerprint, facial_recognition, iris)
|
|
7676
|
+
// Web: returns ['fingerprint'] as generic indicator, or [] if unavailable
|
|
7677
|
+
\`\`\`
|
|
7678
|
+
|
|
7679
|
+
### getSecurityLevel
|
|
7680
|
+
|
|
7681
|
+
Get the security level of biometric authentication on the device.
|
|
7682
|
+
|
|
7683
|
+
\`\`\`tsx
|
|
7684
|
+
await getSecurityLevel(): Promise<SecurityLevel>
|
|
7685
|
+
|
|
7686
|
+
type SecurityLevel = 'none' | 'device_credential' | 'biometric_weak' | 'biometric_strong';
|
|
7687
|
+
|
|
7688
|
+
// Native: maps expo-local-authentication SecurityLevel enum
|
|
7689
|
+
// Web: returns 'biometric_strong' if platform authenticator available, else 'none'
|
|
7690
|
+
\`\`\`
|
|
7691
|
+
|
|
7692
|
+
### authenticate
|
|
7693
|
+
|
|
7694
|
+
Prompt the user for biometric authentication.
|
|
7695
|
+
|
|
7696
|
+
\`\`\`tsx
|
|
7697
|
+
await authenticate(options?: AuthenticateOptions): Promise<AuthResult>
|
|
7698
|
+
\`\`\`
|
|
7699
|
+
|
|
7700
|
+
**AuthenticateOptions:**
|
|
7701
|
+
|
|
7702
|
+
| Option | Type | Default | Description |
|
|
7703
|
+
|--------|------|---------|-------------|
|
|
7704
|
+
| promptMessage | string | 'Authenticate' | Message shown alongside the biometric prompt |
|
|
7705
|
+
| cancelLabel | string | \u2014 | Label for the cancel button |
|
|
7706
|
+
| fallbackLabel | string | \u2014 | iOS: label for passcode fallback button |
|
|
7707
|
+
| disableDeviceFallback | boolean | false | Prevent PIN/passcode fallback after biometric failure |
|
|
7708
|
+
| requireStrongBiometric | boolean | false | Android: require Class 3 (strong) biometric |
|
|
7709
|
+
|
|
7710
|
+
**AuthResult:**
|
|
7711
|
+
|
|
7712
|
+
\`\`\`tsx
|
|
7713
|
+
type AuthResult =
|
|
7714
|
+
| { success: true }
|
|
7715
|
+
| { success: false; error: AuthError; message?: string };
|
|
7716
|
+
|
|
7717
|
+
type AuthError =
|
|
7718
|
+
| 'not_available'
|
|
7719
|
+
| 'not_enrolled'
|
|
7720
|
+
| 'user_cancel'
|
|
7721
|
+
| 'lockout'
|
|
7722
|
+
| 'system_cancel'
|
|
7723
|
+
| 'passcode_not_set'
|
|
7724
|
+
| 'authentication_failed'
|
|
7725
|
+
| 'unknown';
|
|
7726
|
+
\`\`\`
|
|
7727
|
+
|
|
7728
|
+
### cancelAuthentication
|
|
7729
|
+
|
|
7730
|
+
Cancel an in-progress authentication prompt (Android only). No-op on iOS and web.
|
|
7731
|
+
|
|
7732
|
+
\`\`\`tsx
|
|
7733
|
+
await cancelAuthentication(): Promise<void>
|
|
7734
|
+
\`\`\`
|
|
7735
|
+
|
|
7736
|
+
---
|
|
7737
|
+
|
|
7738
|
+
## Passkey Functions
|
|
7739
|
+
|
|
7740
|
+
### isPasskeySupported
|
|
7741
|
+
|
|
7742
|
+
Check if passkeys (WebAuthn/FIDO2) are supported on this device/browser.
|
|
7743
|
+
|
|
7744
|
+
\`\`\`tsx
|
|
7745
|
+
await isPasskeySupported(): Promise<boolean>
|
|
7746
|
+
|
|
7747
|
+
// Web: checks PublicKeyCredential + isUserVerifyingPlatformAuthenticatorAvailable
|
|
7748
|
+
// Native: checks react-native-passkeys Passkey.isSupported()
|
|
7749
|
+
\`\`\`
|
|
7750
|
+
|
|
7751
|
+
### createPasskey
|
|
7752
|
+
|
|
7753
|
+
Create a new passkey credential (registration / attestation ceremony).
|
|
7754
|
+
|
|
7755
|
+
\`\`\`tsx
|
|
7756
|
+
await createPasskey(options: PasskeyCreateOptions): Promise<PasskeyCreateResult>
|
|
7757
|
+
\`\`\`
|
|
7758
|
+
|
|
7759
|
+
**PasskeyCreateOptions:**
|
|
7760
|
+
|
|
7761
|
+
| Option | Type | Required | Description |
|
|
7762
|
+
|--------|------|----------|-------------|
|
|
7763
|
+
| challenge | string | Yes | Base64url-encoded challenge from server |
|
|
7764
|
+
| rp | { id: string; name: string } | Yes | Relying party info |
|
|
7765
|
+
| user | { id: string; name: string; displayName: string } | Yes | User info (id is base64url) |
|
|
7766
|
+
| pubKeyCredParams | PublicKeyCredentialParam[] | No | Defaults to ES256 + RS256 |
|
|
7767
|
+
| timeout | number | No | Timeout in ms (default 60000) |
|
|
7768
|
+
| authenticatorSelection | object | No | Authenticator criteria |
|
|
7769
|
+
| excludeCredentials | CredentialDescriptor[] | No | Prevent re-registration |
|
|
7770
|
+
| attestation | string | No | 'none' \\| 'indirect' \\| 'direct' \\| 'enterprise' |
|
|
7771
|
+
|
|
7772
|
+
**PasskeyCreateResult:**
|
|
7773
|
+
|
|
7774
|
+
| Property | Type | Description |
|
|
7775
|
+
|----------|------|-------------|
|
|
7776
|
+
| id | string | Credential ID (base64url) |
|
|
7777
|
+
| rawId | string | Raw credential ID (base64url) |
|
|
7778
|
+
| type | 'public-key' | Always 'public-key' |
|
|
7779
|
+
| response.clientDataJSON | string | Client data (base64url) |
|
|
7780
|
+
| response.attestationObject | string | Attestation object (base64url) |
|
|
7781
|
+
|
|
7782
|
+
### getPasskey
|
|
7783
|
+
|
|
7784
|
+
Authenticate with an existing passkey (assertion ceremony).
|
|
7785
|
+
|
|
7786
|
+
\`\`\`tsx
|
|
7787
|
+
await getPasskey(options: PasskeyGetOptions): Promise<PasskeyGetResult>
|
|
7788
|
+
\`\`\`
|
|
7789
|
+
|
|
7790
|
+
**PasskeyGetOptions:**
|
|
7791
|
+
|
|
7792
|
+
| Option | Type | Required | Description |
|
|
7793
|
+
|--------|------|----------|-------------|
|
|
7794
|
+
| challenge | string | Yes | Base64url-encoded challenge from server |
|
|
7795
|
+
| rpId | string | No | Relying party ID |
|
|
7796
|
+
| allowCredentials | CredentialDescriptor[] | No | Allowed credentials (empty = discoverable) |
|
|
7797
|
+
| timeout | number | No | Timeout in ms (default 60000) |
|
|
7798
|
+
| userVerification | string | No | 'required' \\| 'preferred' \\| 'discouraged' |
|
|
7799
|
+
|
|
7800
|
+
**PasskeyGetResult:**
|
|
7801
|
+
|
|
7802
|
+
| Property | Type | Description |
|
|
7803
|
+
|----------|------|-------------|
|
|
7804
|
+
| id | string | Credential ID (base64url) |
|
|
7805
|
+
| rawId | string | Raw credential ID (base64url) |
|
|
7806
|
+
| type | 'public-key' | Always 'public-key' |
|
|
7807
|
+
| response.clientDataJSON | string | Client data (base64url) |
|
|
7808
|
+
| response.authenticatorData | string | Authenticator data (base64url) |
|
|
7809
|
+
| response.signature | string | Signature (base64url) |
|
|
7810
|
+
| response.userHandle | string \\| undefined | User handle (base64url) |
|
|
7811
|
+
|
|
7812
|
+
### PasskeyError
|
|
7813
|
+
|
|
7814
|
+
Both \`createPasskey\` and \`getPasskey\` throw a \`PasskeyError\` on failure:
|
|
7815
|
+
|
|
7816
|
+
\`\`\`tsx
|
|
7817
|
+
interface PasskeyError {
|
|
7818
|
+
code: 'not_supported' | 'cancelled' | 'invalid_state' | 'not_allowed' | 'unknown';
|
|
7819
|
+
message: string;
|
|
7820
|
+
}
|
|
7821
|
+
|
|
7822
|
+
try {
|
|
7823
|
+
const result = await createPasskey(options);
|
|
7824
|
+
} catch (err) {
|
|
7825
|
+
const passkeyErr = err as PasskeyError;
|
|
7826
|
+
console.log(passkeyErr.code, passkeyErr.message);
|
|
7827
|
+
}
|
|
7828
|
+
\`\`\`
|
|
7829
|
+
|
|
7830
|
+
---
|
|
7831
|
+
|
|
7832
|
+
## Base64url Helpers
|
|
7833
|
+
|
|
7834
|
+
Shared utilities for encoding/decoding WebAuthn binary data.
|
|
7835
|
+
|
|
7836
|
+
\`\`\`tsx
|
|
7837
|
+
import { base64urlToBuffer, bufferToBase64url } from '@idealyst/biometrics';
|
|
7838
|
+
|
|
7839
|
+
// Convert base64url string to ArrayBuffer
|
|
7840
|
+
const buffer: ArrayBuffer = base64urlToBuffer(base64urlString);
|
|
7841
|
+
|
|
7842
|
+
// Convert ArrayBuffer to base64url string
|
|
7843
|
+
const str: string = bufferToBase64url(arrayBuffer);
|
|
7844
|
+
\`\`\`
|
|
7845
|
+
`,
|
|
7846
|
+
"idealyst://biometrics/examples": `# Biometrics Examples
|
|
7847
|
+
|
|
7848
|
+
Complete code examples for common @idealyst/biometrics patterns.
|
|
7849
|
+
|
|
7850
|
+
## Gate Screen Access with Biometrics
|
|
7851
|
+
|
|
7852
|
+
\`\`\`tsx
|
|
7853
|
+
import { isBiometricAvailable, authenticate } from '@idealyst/biometrics';
|
|
7854
|
+
import { useEffect, useState } from 'react';
|
|
7855
|
+
|
|
7856
|
+
function ProtectedScreen({ children }: { children: React.ReactNode }) {
|
|
7857
|
+
const [unlocked, setUnlocked] = useState(false);
|
|
7858
|
+
const [error, setError] = useState<string | null>(null);
|
|
7859
|
+
|
|
7860
|
+
const unlock = async () => {
|
|
7861
|
+
const available = await isBiometricAvailable();
|
|
7862
|
+
if (!available) {
|
|
7863
|
+
// No biometrics \u2014 fall back to PIN or allow access
|
|
7864
|
+
setUnlocked(true);
|
|
7865
|
+
return;
|
|
7866
|
+
}
|
|
7867
|
+
|
|
7868
|
+
const result = await authenticate({
|
|
7869
|
+
promptMessage: 'Unlock to continue',
|
|
7870
|
+
cancelLabel: 'Cancel',
|
|
7871
|
+
});
|
|
7872
|
+
|
|
7873
|
+
if (result.success) {
|
|
7874
|
+
setUnlocked(true);
|
|
7875
|
+
} else {
|
|
7876
|
+
setError(result.message ?? 'Authentication failed');
|
|
7877
|
+
}
|
|
7878
|
+
};
|
|
7879
|
+
|
|
7880
|
+
useEffect(() => {
|
|
7881
|
+
unlock();
|
|
7882
|
+
}, []);
|
|
7883
|
+
|
|
7884
|
+
if (!unlocked) {
|
|
7885
|
+
return (
|
|
7886
|
+
<View>
|
|
7887
|
+
<Text>Please authenticate to continue</Text>
|
|
7888
|
+
{error && <Text intent="negative">{error}</Text>}
|
|
7889
|
+
<Button label="Try Again" onPress={unlock} />
|
|
7890
|
+
</View>
|
|
7891
|
+
);
|
|
7892
|
+
}
|
|
7893
|
+
|
|
7894
|
+
return <>{children}</>;
|
|
7895
|
+
}
|
|
7896
|
+
\`\`\`
|
|
7897
|
+
|
|
7898
|
+
## Confirm Sensitive Action
|
|
7899
|
+
|
|
7900
|
+
\`\`\`tsx
|
|
7901
|
+
import { authenticate } from '@idealyst/biometrics';
|
|
7902
|
+
|
|
7903
|
+
async function confirmTransfer(amount: number, recipient: string) {
|
|
7904
|
+
const result = await authenticate({
|
|
7905
|
+
promptMessage: \`Confirm transfer of $\${amount} to \${recipient}\`,
|
|
7906
|
+
disableDeviceFallback: false,
|
|
7907
|
+
});
|
|
7908
|
+
|
|
7909
|
+
if (!result.success) {
|
|
7910
|
+
throw new Error(result.message ?? 'Authentication required');
|
|
7911
|
+
}
|
|
7912
|
+
|
|
7913
|
+
// Proceed with transfer
|
|
7914
|
+
await api.transfer({ amount, recipient });
|
|
7915
|
+
}
|
|
7916
|
+
\`\`\`
|
|
7917
|
+
|
|
7918
|
+
## Show Biometric Info
|
|
7919
|
+
|
|
7920
|
+
\`\`\`tsx
|
|
7921
|
+
import {
|
|
7922
|
+
isBiometricAvailable,
|
|
7923
|
+
getBiometricTypes,
|
|
7924
|
+
getSecurityLevel,
|
|
7925
|
+
} from '@idealyst/biometrics';
|
|
7926
|
+
|
|
7927
|
+
function BiometricSettings() {
|
|
7928
|
+
const [info, setInfo] = useState({
|
|
7929
|
+
available: false,
|
|
7930
|
+
types: [] as string[],
|
|
7931
|
+
level: 'none',
|
|
7932
|
+
});
|
|
7933
|
+
|
|
7934
|
+
useEffect(() => {
|
|
7935
|
+
async function load() {
|
|
7936
|
+
const [available, types, level] = await Promise.all([
|
|
7937
|
+
isBiometricAvailable(),
|
|
7938
|
+
getBiometricTypes(),
|
|
7939
|
+
getSecurityLevel(),
|
|
7940
|
+
]);
|
|
7941
|
+
setInfo({ available, types, level });
|
|
7942
|
+
}
|
|
7943
|
+
load();
|
|
7944
|
+
}, []);
|
|
7945
|
+
|
|
7946
|
+
return (
|
|
7947
|
+
<Card>
|
|
7948
|
+
<Text>Biometric available: {info.available ? 'Yes' : 'No'}</Text>
|
|
7949
|
+
<Text>Types: {info.types.join(', ') || 'None'}</Text>
|
|
7950
|
+
<Text>Security level: {info.level}</Text>
|
|
7951
|
+
</Card>
|
|
7952
|
+
);
|
|
7953
|
+
}
|
|
7954
|
+
\`\`\`
|
|
7955
|
+
|
|
7956
|
+
## Passkey Registration Flow
|
|
7957
|
+
|
|
7958
|
+
\`\`\`tsx
|
|
7959
|
+
import { isPasskeySupported, createPasskey } from '@idealyst/biometrics';
|
|
7960
|
+
import type { PasskeyError } from '@idealyst/biometrics';
|
|
7961
|
+
|
|
7962
|
+
async function registerPasskey(user: { id: string; email: string; name: string }) {
|
|
7963
|
+
const supported = await isPasskeySupported();
|
|
7964
|
+
if (!supported) {
|
|
7965
|
+
alert('Passkeys are not supported on this device');
|
|
7966
|
+
return;
|
|
7967
|
+
}
|
|
7968
|
+
|
|
7969
|
+
// 1. Get challenge from server
|
|
7970
|
+
const { challenge, rpId, rpName } = await api.getRegistrationChallenge();
|
|
7971
|
+
|
|
7972
|
+
try {
|
|
7973
|
+
// 2. Create the passkey
|
|
7974
|
+
const credential = await createPasskey({
|
|
7975
|
+
challenge,
|
|
7976
|
+
rp: { id: rpId, name: rpName },
|
|
7977
|
+
user: {
|
|
7978
|
+
id: user.id,
|
|
7979
|
+
name: user.email,
|
|
7980
|
+
displayName: user.name,
|
|
7981
|
+
},
|
|
7982
|
+
authenticatorSelection: {
|
|
7983
|
+
residentKey: 'required',
|
|
7984
|
+
userVerification: 'required',
|
|
7985
|
+
},
|
|
7986
|
+
});
|
|
7987
|
+
|
|
7988
|
+
// 3. Send to server for verification and storage
|
|
7989
|
+
await api.verifyRegistration({
|
|
7990
|
+
id: credential.id,
|
|
7991
|
+
rawId: credential.rawId,
|
|
7992
|
+
clientDataJSON: credential.response.clientDataJSON,
|
|
7993
|
+
attestationObject: credential.response.attestationObject,
|
|
7994
|
+
});
|
|
7995
|
+
|
|
7996
|
+
alert('Passkey registered successfully!');
|
|
7997
|
+
} catch (err) {
|
|
7998
|
+
const passkeyErr = err as PasskeyError;
|
|
7999
|
+
if (passkeyErr.code === 'cancelled') {
|
|
8000
|
+
// User cancelled \u2014 do nothing
|
|
8001
|
+
return;
|
|
8002
|
+
}
|
|
8003
|
+
alert(\`Failed to register passkey: \${passkeyErr.message}\`);
|
|
8004
|
+
}
|
|
8005
|
+
}
|
|
8006
|
+
\`\`\`
|
|
8007
|
+
|
|
8008
|
+
## Passkey Login Flow
|
|
8009
|
+
|
|
8010
|
+
\`\`\`tsx
|
|
8011
|
+
import { getPasskey } from '@idealyst/biometrics';
|
|
8012
|
+
import type { PasskeyError } from '@idealyst/biometrics';
|
|
8013
|
+
|
|
8014
|
+
async function loginWithPasskey() {
|
|
8015
|
+
// 1. Get challenge from server
|
|
8016
|
+
const { challenge, rpId } = await api.getAuthenticationChallenge();
|
|
8017
|
+
|
|
8018
|
+
try {
|
|
8019
|
+
// 2. Authenticate with passkey
|
|
8020
|
+
const assertion = await getPasskey({
|
|
8021
|
+
challenge,
|
|
8022
|
+
rpId,
|
|
8023
|
+
userVerification: 'required',
|
|
8024
|
+
});
|
|
8025
|
+
|
|
8026
|
+
// 3. Send to server for verification
|
|
8027
|
+
const session = await api.verifyAuthentication({
|
|
8028
|
+
id: assertion.id,
|
|
8029
|
+
rawId: assertion.rawId,
|
|
8030
|
+
clientDataJSON: assertion.response.clientDataJSON,
|
|
8031
|
+
authenticatorData: assertion.response.authenticatorData,
|
|
8032
|
+
signature: assertion.response.signature,
|
|
8033
|
+
userHandle: assertion.response.userHandle,
|
|
8034
|
+
});
|
|
8035
|
+
|
|
8036
|
+
return session;
|
|
8037
|
+
} catch (err) {
|
|
8038
|
+
const passkeyErr = err as PasskeyError;
|
|
8039
|
+
if (passkeyErr.code === 'cancelled') return null;
|
|
8040
|
+
throw passkeyErr;
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
8043
|
+
\`\`\`
|
|
8044
|
+
|
|
8045
|
+
## Login Screen with Passkey + Fallback
|
|
8046
|
+
|
|
8047
|
+
\`\`\`tsx
|
|
8048
|
+
import { isPasskeySupported, getPasskey } from '@idealyst/biometrics';
|
|
8049
|
+
import type { PasskeyError } from '@idealyst/biometrics';
|
|
8050
|
+
|
|
8051
|
+
function LoginScreen() {
|
|
8052
|
+
const [passkeyAvailable, setPasskeyAvailable] = useState(false);
|
|
8053
|
+
const [loading, setLoading] = useState(false);
|
|
8054
|
+
|
|
8055
|
+
useEffect(() => {
|
|
8056
|
+
isPasskeySupported().then(setPasskeyAvailable);
|
|
8057
|
+
}, []);
|
|
8058
|
+
|
|
8059
|
+
const handlePasskeyLogin = async () => {
|
|
8060
|
+
setLoading(true);
|
|
8061
|
+
try {
|
|
8062
|
+
const { challenge, rpId } = await api.getAuthenticationChallenge();
|
|
8063
|
+
const assertion = await getPasskey({ challenge, rpId });
|
|
8064
|
+
const session = await api.verifyAuthentication(assertion);
|
|
8065
|
+
navigateToHome(session);
|
|
8066
|
+
} catch (err) {
|
|
8067
|
+
const e = err as PasskeyError;
|
|
8068
|
+
if (e.code !== 'cancelled') {
|
|
8069
|
+
showError(e.message);
|
|
8070
|
+
}
|
|
8071
|
+
} finally {
|
|
8072
|
+
setLoading(false);
|
|
8073
|
+
}
|
|
8074
|
+
};
|
|
8075
|
+
|
|
8076
|
+
return (
|
|
8077
|
+
<View>
|
|
8078
|
+
<Text variant="headline">Welcome Back</Text>
|
|
8079
|
+
|
|
8080
|
+
{passkeyAvailable && (
|
|
8081
|
+
<Button
|
|
8082
|
+
label="Sign in with Passkey"
|
|
8083
|
+
iconName="fingerprint"
|
|
8084
|
+
onPress={handlePasskeyLogin}
|
|
8085
|
+
loading={loading}
|
|
8086
|
+
/>
|
|
8087
|
+
)}
|
|
8088
|
+
|
|
8089
|
+
<Button
|
|
8090
|
+
label="Sign in with Email"
|
|
8091
|
+
intent="neutral"
|
|
8092
|
+
onPress={navigateToEmailLogin}
|
|
8093
|
+
/>
|
|
8094
|
+
</View>
|
|
8095
|
+
);
|
|
8096
|
+
}
|
|
8097
|
+
\`\`\`
|
|
8098
|
+
|
|
8099
|
+
## Best Practices
|
|
8100
|
+
|
|
8101
|
+
1. **Always check availability first** \u2014 Call \`isBiometricAvailable()\` or \`isPasskeySupported()\` before prompting
|
|
8102
|
+
2. **Provide fallbacks** \u2014 Not all devices support biometrics. Offer PIN/password alternatives
|
|
8103
|
+
3. **Handle cancellation gracefully** \u2014 Users may cancel the prompt. Don't show errors for \`user_cancel\` / \`cancelled\`
|
|
8104
|
+
4. **Use try/catch for passkeys** \u2014 Passkey functions throw \`PasskeyError\` on failure
|
|
8105
|
+
5. **Server-side validation** \u2014 Always verify passkey responses on your server. The client is untrusted
|
|
8106
|
+
6. **Base64url encoding** \u2014 All binary WebAuthn data is encoded as base64url strings for transport
|
|
8107
|
+
7. **Set rpId correctly** \u2014 The relying party ID must match your domain. On native, it's your associated domain
|
|
8108
|
+
8. **Lockout handling** \u2014 After too many failed biometric attempts, the device locks out. Handle the \`lockout\` error
|
|
8109
|
+
9. **iOS permissions** \u2014 FaceID requires \`NSFaceIDUsageDescription\` in Info.plist
|
|
8110
|
+
10. **Android associated domains** \u2014 Passkeys on Android require a Digital Asset Links file at \`/.well-known/assetlinks.json\`
|
|
8111
|
+
`
|
|
8112
|
+
};
|
|
8113
|
+
|
|
8114
|
+
// src/data/payments-guides.ts
|
|
8115
|
+
var paymentsGuides = {
|
|
8116
|
+
"idealyst://payments/overview": `# @idealyst/payments Overview
|
|
8117
|
+
|
|
8118
|
+
Cross-platform payment provider abstractions for React and React Native. Wraps Stripe's Platform Pay API for Apple Pay and Google Pay on mobile.
|
|
8119
|
+
|
|
8120
|
+
## Features
|
|
8121
|
+
|
|
8122
|
+
- **Apple Pay** \u2014 Native iOS payment sheet via Stripe SDK
|
|
8123
|
+
- **Google Pay** \u2014 Native Android payment sheet via Stripe SDK
|
|
8124
|
+
- **Cross-Platform** \u2014 Single API for React Native and web (web is stub/noop)
|
|
8125
|
+
- **Flat Functions + Hook** \u2014 Use \`initializePayments()\`, \`confirmPayment()\` directly, or \`usePayments()\` hook
|
|
8126
|
+
- **Stripe Platform Pay** \u2014 Wraps \`@stripe/stripe-react-native\` Platform Pay API
|
|
8127
|
+
- **Graceful Degradation** \u2014 Falls back cleanly when Stripe SDK isn't installed
|
|
8128
|
+
- **TypeScript** \u2014 Full type safety
|
|
8129
|
+
|
|
8130
|
+
## Installation
|
|
8131
|
+
|
|
8132
|
+
\`\`\`bash
|
|
8133
|
+
yarn add @idealyst/payments
|
|
8134
|
+
|
|
8135
|
+
# React Native \u2014 required for mobile payments:
|
|
8136
|
+
yarn add @stripe/stripe-react-native
|
|
8137
|
+
cd ios && pod install
|
|
8138
|
+
\`\`\`
|
|
8139
|
+
|
|
8140
|
+
## Web Support
|
|
8141
|
+
|
|
8142
|
+
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\`).
|
|
8143
|
+
|
|
8144
|
+
## Quick Start
|
|
8145
|
+
|
|
8146
|
+
\`\`\`tsx
|
|
8147
|
+
import { usePayments } from '@idealyst/payments';
|
|
8148
|
+
|
|
8149
|
+
function CheckoutScreen() {
|
|
8150
|
+
const {
|
|
8151
|
+
isReady,
|
|
8152
|
+
isApplePayAvailable,
|
|
8153
|
+
isGooglePayAvailable,
|
|
8154
|
+
isProcessing,
|
|
8155
|
+
confirmPayment,
|
|
8156
|
+
} = usePayments({
|
|
8157
|
+
config: {
|
|
8158
|
+
publishableKey: 'pk_test_xxx',
|
|
8159
|
+
merchantIdentifier: 'merchant.com.myapp',
|
|
8160
|
+
merchantName: 'My App',
|
|
8161
|
+
merchantCountryCode: 'US',
|
|
8162
|
+
},
|
|
8163
|
+
});
|
|
8164
|
+
|
|
8165
|
+
const handlePay = async () => {
|
|
8166
|
+
const result = await confirmPayment({
|
|
8167
|
+
clientSecret: 'pi_xxx_secret_xxx', // from server
|
|
8168
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
8169
|
+
});
|
|
8170
|
+
console.log('Paid:', result.paymentIntentId);
|
|
8171
|
+
};
|
|
8172
|
+
|
|
8173
|
+
if (!isReady) return null;
|
|
8174
|
+
|
|
8175
|
+
return (
|
|
8176
|
+
<Button
|
|
8177
|
+
onPress={handlePay}
|
|
8178
|
+
disabled={isProcessing}
|
|
8179
|
+
label={isApplePayAvailable ? 'Apple Pay' : 'Google Pay'}
|
|
8180
|
+
/>
|
|
8181
|
+
);
|
|
8182
|
+
}
|
|
8183
|
+
\`\`\`
|
|
8184
|
+
|
|
8185
|
+
## Platform Support
|
|
8186
|
+
|
|
8187
|
+
| Feature | iOS | Android | Web |
|
|
8188
|
+
|---------|-----|---------|-----|
|
|
8189
|
+
| Apple Pay | Yes | \u2014 | \u2014 |
|
|
8190
|
+
| Google Pay | \u2014 | Yes | \u2014 |
|
|
8191
|
+
| usePayments hook | Yes | Yes | Stub |
|
|
8192
|
+
| confirmPayment | Yes | Yes | Throws |
|
|
8193
|
+
| createPaymentMethod | Yes | Yes | Throws |
|
|
8194
|
+
`,
|
|
8195
|
+
"idealyst://payments/api": `# Payments API Reference
|
|
8196
|
+
|
|
8197
|
+
Complete API reference for @idealyst/payments.
|
|
8198
|
+
|
|
8199
|
+
---
|
|
8200
|
+
|
|
8201
|
+
## Flat Functions
|
|
8202
|
+
|
|
8203
|
+
### initializePayments
|
|
8204
|
+
|
|
8205
|
+
Initialize the Stripe SDK for platform payments.
|
|
8206
|
+
|
|
8207
|
+
\`\`\`tsx
|
|
8208
|
+
import { initializePayments } from '@idealyst/payments';
|
|
8209
|
+
|
|
8210
|
+
await initializePayments({
|
|
8211
|
+
publishableKey: 'pk_test_xxx',
|
|
8212
|
+
merchantIdentifier: 'merchant.com.myapp', // iOS Apple Pay
|
|
8213
|
+
merchantName: 'My App',
|
|
8214
|
+
merchantCountryCode: 'US',
|
|
8215
|
+
urlScheme: 'com.myapp', // for 3D Secure redirects
|
|
8216
|
+
testEnvironment: true, // Google Pay test mode
|
|
8217
|
+
});
|
|
8218
|
+
\`\`\`
|
|
8219
|
+
|
|
8220
|
+
**PaymentConfig:**
|
|
8221
|
+
|
|
8222
|
+
| Option | Type | Required | Description |
|
|
8223
|
+
|--------|------|----------|-------------|
|
|
8224
|
+
| publishableKey | string | Yes | Stripe publishable key |
|
|
8225
|
+
| merchantIdentifier | string | No | Apple Pay merchant ID (iOS) |
|
|
8226
|
+
| merchantName | string | Yes | Display name on payment sheet |
|
|
8227
|
+
| merchantCountryCode | string | Yes | ISO 3166-1 alpha-2 country |
|
|
8228
|
+
| urlScheme | string | No | URL scheme for 3D Secure |
|
|
8229
|
+
| testEnvironment | boolean | No | Google Pay test mode (default: false) |
|
|
8230
|
+
|
|
8231
|
+
### checkPaymentAvailability
|
|
8232
|
+
|
|
8233
|
+
Check which payment methods are available on the current device.
|
|
8234
|
+
|
|
8235
|
+
\`\`\`tsx
|
|
8236
|
+
import { checkPaymentAvailability } from '@idealyst/payments';
|
|
8237
|
+
|
|
8238
|
+
const methods = await checkPaymentAvailability();
|
|
8239
|
+
// [
|
|
8240
|
+
// { type: 'apple_pay', isAvailable: true },
|
|
8241
|
+
// { type: 'google_pay', isAvailable: false, unavailableReason: '...' },
|
|
8242
|
+
// { type: 'card', isAvailable: true },
|
|
8243
|
+
// ]
|
|
8244
|
+
\`\`\`
|
|
8245
|
+
|
|
8246
|
+
### confirmPayment
|
|
8247
|
+
|
|
8248
|
+
Present the platform payment sheet and confirm a PaymentIntent. Requires a \`clientSecret\` from a server-created PaymentIntent.
|
|
8249
|
+
|
|
8250
|
+
\`\`\`tsx
|
|
8251
|
+
import { confirmPayment } from '@idealyst/payments';
|
|
8252
|
+
|
|
8253
|
+
const result = await confirmPayment({
|
|
8254
|
+
clientSecret: 'pi_xxx_secret_xxx',
|
|
8255
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
8256
|
+
lineItems: [
|
|
8257
|
+
{ label: 'Widget', amount: 999 },
|
|
8258
|
+
{ label: 'Tax', amount: 100 },
|
|
8259
|
+
],
|
|
8260
|
+
billingAddress: {
|
|
8261
|
+
isRequired: true,
|
|
8262
|
+
format: 'FULL',
|
|
8263
|
+
},
|
|
8264
|
+
});
|
|
8265
|
+
|
|
8266
|
+
console.log(result.paymentIntentId); // 'pi_xxx'
|
|
8267
|
+
console.log(result.status); // 'succeeded'
|
|
8268
|
+
\`\`\`
|
|
8269
|
+
|
|
8270
|
+
### createPaymentMethod
|
|
8271
|
+
|
|
8272
|
+
Present the payment sheet and create a payment method without confirming. Returns a payment method ID for server-side processing.
|
|
8273
|
+
|
|
8274
|
+
\`\`\`tsx
|
|
8275
|
+
import { createPaymentMethod } from '@idealyst/payments';
|
|
8276
|
+
|
|
8277
|
+
const result = await createPaymentMethod({
|
|
8278
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
8279
|
+
});
|
|
8280
|
+
|
|
8281
|
+
console.log(result.paymentMethodId); // 'pm_xxx'
|
|
8282
|
+
// Send to your server for processing
|
|
8283
|
+
\`\`\`
|
|
8284
|
+
|
|
8285
|
+
### getPaymentStatus
|
|
8286
|
+
|
|
8287
|
+
Get the current payment provider status.
|
|
8288
|
+
|
|
8289
|
+
\`\`\`tsx
|
|
8290
|
+
import { getPaymentStatus } from '@idealyst/payments';
|
|
8291
|
+
|
|
8292
|
+
const status = getPaymentStatus();
|
|
8293
|
+
// { state: 'ready', availablePaymentMethods: [...], isPaymentAvailable: true }
|
|
8294
|
+
\`\`\`
|
|
8295
|
+
|
|
8296
|
+
---
|
|
8297
|
+
|
|
8298
|
+
## usePayments Hook
|
|
8299
|
+
|
|
8300
|
+
Convenience hook that wraps the flat functions with React state management.
|
|
8301
|
+
|
|
8302
|
+
\`\`\`tsx
|
|
8303
|
+
import { usePayments } from '@idealyst/payments';
|
|
8304
|
+
|
|
8305
|
+
const {
|
|
8306
|
+
// State
|
|
8307
|
+
status, // PaymentProviderStatus
|
|
8308
|
+
isReady, // boolean
|
|
8309
|
+
isProcessing, // boolean
|
|
8310
|
+
availablePaymentMethods, // PaymentMethodAvailability[]
|
|
8311
|
+
isApplePayAvailable, // boolean
|
|
8312
|
+
isGooglePayAvailable,// boolean
|
|
8313
|
+
isPaymentAvailable, // boolean
|
|
8314
|
+
error, // PaymentError | null
|
|
8315
|
+
|
|
8316
|
+
// Actions
|
|
8317
|
+
initialize, // (config: PaymentConfig) => Promise<void>
|
|
8318
|
+
checkAvailability, // () => Promise<PaymentMethodAvailability[]>
|
|
8319
|
+
confirmPayment, // (request: PaymentSheetRequest) => Promise<PaymentResult>
|
|
8320
|
+
createPaymentMethod, // (request: PaymentSheetRequest) => Promise<PaymentResult>
|
|
8321
|
+
clearError, // () => void
|
|
8322
|
+
} = usePayments(options?);
|
|
8323
|
+
\`\`\`
|
|
8324
|
+
|
|
8325
|
+
**UsePaymentsOptions:**
|
|
8326
|
+
|
|
8327
|
+
| Option | Type | Default | Description |
|
|
8328
|
+
|--------|------|---------|-------------|
|
|
8329
|
+
| config | PaymentConfig | \u2014 | Auto-initialize with this config |
|
|
8330
|
+
| autoCheckAvailability | boolean | true | Check availability after init |
|
|
8331
|
+
|
|
8332
|
+
---
|
|
8333
|
+
|
|
8334
|
+
## Types
|
|
8335
|
+
|
|
8336
|
+
### PaymentMethodType
|
|
8337
|
+
|
|
8338
|
+
\`\`\`tsx
|
|
8339
|
+
type PaymentMethodType = 'apple_pay' | 'google_pay' | 'card';
|
|
8340
|
+
\`\`\`
|
|
8341
|
+
|
|
8342
|
+
### PaymentAmount
|
|
8343
|
+
|
|
8344
|
+
\`\`\`tsx
|
|
8345
|
+
interface PaymentAmount {
|
|
8346
|
+
amount: number; // smallest currency unit (cents)
|
|
8347
|
+
currencyCode: string; // ISO 4217 (e.g., 'usd')
|
|
8348
|
+
}
|
|
8349
|
+
\`\`\`
|
|
8350
|
+
|
|
8351
|
+
### PaymentSheetRequest
|
|
8352
|
+
|
|
8353
|
+
\`\`\`tsx
|
|
8354
|
+
interface PaymentSheetRequest {
|
|
8355
|
+
clientSecret?: string; // required for confirmPayment
|
|
8356
|
+
amount: PaymentAmount;
|
|
8357
|
+
lineItems?: PaymentLineItem[];
|
|
8358
|
+
billingAddress?: BillingAddressConfig;
|
|
8359
|
+
allowedPaymentMethods?: PaymentMethodType[];
|
|
8360
|
+
}
|
|
8361
|
+
\`\`\`
|
|
8362
|
+
|
|
8363
|
+
### PaymentResult
|
|
8364
|
+
|
|
8365
|
+
\`\`\`tsx
|
|
8366
|
+
interface PaymentResult {
|
|
8367
|
+
paymentMethodType: PaymentMethodType;
|
|
8368
|
+
paymentMethodId?: string; // for createPaymentMethod
|
|
8369
|
+
paymentIntentId?: string; // for confirmPayment
|
|
8370
|
+
status?: string; // 'succeeded', 'requires_capture', etc.
|
|
8371
|
+
}
|
|
8372
|
+
\`\`\`
|
|
8373
|
+
|
|
8374
|
+
### PaymentError
|
|
6654
8375
|
|
|
6655
8376
|
\`\`\`tsx
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
8377
|
+
interface PaymentError {
|
|
8378
|
+
code: PaymentErrorCode;
|
|
8379
|
+
message: string;
|
|
8380
|
+
originalError?: unknown;
|
|
8381
|
+
}
|
|
8382
|
+
|
|
8383
|
+
type PaymentErrorCode =
|
|
8384
|
+
| 'not_initialized'
|
|
8385
|
+
| 'not_available'
|
|
8386
|
+
| 'not_supported'
|
|
8387
|
+
| 'user_cancelled'
|
|
8388
|
+
| 'payment_failed'
|
|
8389
|
+
| 'network_error'
|
|
8390
|
+
| 'invalid_config'
|
|
8391
|
+
| 'invalid_request'
|
|
8392
|
+
| 'provider_error'
|
|
8393
|
+
| 'unknown';
|
|
8394
|
+
\`\`\`
|
|
8395
|
+
`,
|
|
8396
|
+
"idealyst://payments/examples": `# Payments Examples
|
|
6659
8397
|
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
8398
|
+
Complete code examples for common @idealyst/payments patterns.
|
|
8399
|
+
|
|
8400
|
+
## Basic Checkout with Hook
|
|
8401
|
+
|
|
8402
|
+
\`\`\`tsx
|
|
8403
|
+
import { usePayments } from '@idealyst/payments';
|
|
8404
|
+
import { Button, View, Text } from '@idealyst/components';
|
|
8405
|
+
|
|
8406
|
+
function CheckoutScreen({ clientSecret }: { clientSecret: string }) {
|
|
8407
|
+
const {
|
|
8408
|
+
isReady,
|
|
8409
|
+
isApplePayAvailable,
|
|
8410
|
+
isGooglePayAvailable,
|
|
8411
|
+
isProcessing,
|
|
8412
|
+
error,
|
|
8413
|
+
confirmPayment,
|
|
8414
|
+
clearError,
|
|
8415
|
+
} = usePayments({
|
|
8416
|
+
config: {
|
|
8417
|
+
publishableKey: 'pk_test_xxx',
|
|
8418
|
+
merchantIdentifier: 'merchant.com.myapp',
|
|
8419
|
+
merchantName: 'My App',
|
|
8420
|
+
merchantCountryCode: 'US',
|
|
8421
|
+
testEnvironment: __DEV__,
|
|
8422
|
+
},
|
|
8423
|
+
});
|
|
8424
|
+
|
|
8425
|
+
const handlePay = async () => {
|
|
8426
|
+
try {
|
|
8427
|
+
const result = await confirmPayment({
|
|
8428
|
+
clientSecret,
|
|
8429
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
8430
|
+
lineItems: [
|
|
8431
|
+
{ label: 'Widget', amount: 999 },
|
|
8432
|
+
{ label: 'Tax', amount: 100 },
|
|
8433
|
+
],
|
|
8434
|
+
});
|
|
8435
|
+
|
|
8436
|
+
// Payment succeeded
|
|
8437
|
+
console.log('Payment confirmed:', result.paymentIntentId);
|
|
8438
|
+
} catch (err) {
|
|
8439
|
+
// Error is automatically set in hook state
|
|
8440
|
+
console.error('Payment failed:', err);
|
|
8441
|
+
}
|
|
8442
|
+
};
|
|
8443
|
+
|
|
8444
|
+
if (!isReady) {
|
|
8445
|
+
return <Text>Loading payment methods...</Text>;
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
const canPay = isApplePayAvailable || isGooglePayAvailable;
|
|
6685
8449
|
|
|
6686
|
-
function ComparisonChart() {
|
|
6687
8450
|
return (
|
|
6688
|
-
<
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
8451
|
+
<View>
|
|
8452
|
+
{canPay ? (
|
|
8453
|
+
<Button
|
|
8454
|
+
onPress={handlePay}
|
|
8455
|
+
disabled={isProcessing}
|
|
8456
|
+
intent="primary"
|
|
8457
|
+
>
|
|
8458
|
+
{isApplePayAvailable ? 'Pay with Apple Pay' : 'Pay with Google Pay'}
|
|
8459
|
+
</Button>
|
|
8460
|
+
) : (
|
|
8461
|
+
<Text>No payment methods available</Text>
|
|
8462
|
+
)}
|
|
8463
|
+
|
|
8464
|
+
{error && (
|
|
8465
|
+
<View>
|
|
8466
|
+
<Text intent="danger">{error.message}</Text>
|
|
8467
|
+
<Button onPress={clearError}>Dismiss</Button>
|
|
8468
|
+
</View>
|
|
8469
|
+
)}
|
|
8470
|
+
</View>
|
|
6695
8471
|
);
|
|
6696
8472
|
}
|
|
6697
8473
|
\`\`\`
|
|
6698
8474
|
|
|
6699
|
-
##
|
|
8475
|
+
## Create Payment Method (Server-Side Confirm)
|
|
6700
8476
|
|
|
6701
8477
|
\`\`\`tsx
|
|
6702
|
-
import
|
|
6703
|
-
|
|
6704
|
-
|
|
8478
|
+
import { usePayments } from '@idealyst/payments';
|
|
8479
|
+
|
|
8480
|
+
function DonateScreen() {
|
|
8481
|
+
const { isReady, isPaymentAvailable, createPaymentMethod } = usePayments({
|
|
8482
|
+
config: {
|
|
8483
|
+
publishableKey: 'pk_test_xxx',
|
|
8484
|
+
merchantName: 'Charity',
|
|
8485
|
+
merchantCountryCode: 'US',
|
|
8486
|
+
},
|
|
8487
|
+
});
|
|
6705
8488
|
|
|
6706
|
-
const
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
8489
|
+
const handleDonate = async (amountCents: number) => {
|
|
8490
|
+
const result = await createPaymentMethod({
|
|
8491
|
+
amount: { amount: amountCents, currencyCode: 'usd' },
|
|
8492
|
+
});
|
|
8493
|
+
|
|
8494
|
+
// Send payment method to your server
|
|
8495
|
+
await fetch('/api/donate', {
|
|
8496
|
+
method: 'POST',
|
|
8497
|
+
body: JSON.stringify({
|
|
8498
|
+
paymentMethodId: result.paymentMethodId,
|
|
8499
|
+
amount: amountCents,
|
|
8500
|
+
}),
|
|
8501
|
+
});
|
|
8502
|
+
};
|
|
6713
8503
|
|
|
6714
|
-
function CategoryBreakdown() {
|
|
6715
8504
|
return (
|
|
6716
|
-
<View
|
|
6717
|
-
<
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
yAxis={{ tickFormat: (value: number | string | Date) => \`\${value} units\` }}
|
|
6724
|
-
/>
|
|
8505
|
+
<View>
|
|
8506
|
+
<Button onPress={() => handleDonate(500)} disabled={!isPaymentAvailable}>
|
|
8507
|
+
Donate $5
|
|
8508
|
+
</Button>
|
|
8509
|
+
<Button onPress={() => handleDonate(1000)} disabled={!isPaymentAvailable}>
|
|
8510
|
+
Donate $10
|
|
8511
|
+
</Button>
|
|
6725
8512
|
</View>
|
|
6726
8513
|
);
|
|
6727
8514
|
}
|
|
6728
8515
|
\`\`\`
|
|
6729
8516
|
|
|
6730
|
-
##
|
|
8517
|
+
## Flat Functions (No Hook)
|
|
6731
8518
|
|
|
6732
8519
|
\`\`\`tsx
|
|
6733
|
-
import
|
|
6734
|
-
|
|
8520
|
+
import {
|
|
8521
|
+
initializePayments,
|
|
8522
|
+
checkPaymentAvailability,
|
|
8523
|
+
confirmPayment,
|
|
8524
|
+
getPaymentStatus,
|
|
8525
|
+
} from '@idealyst/payments';
|
|
8526
|
+
import type { PaymentError } from '@idealyst/payments';
|
|
8527
|
+
|
|
8528
|
+
// Initialize once at app startup
|
|
8529
|
+
async function setupPayments() {
|
|
8530
|
+
await initializePayments({
|
|
8531
|
+
publishableKey: 'pk_test_xxx',
|
|
8532
|
+
merchantIdentifier: 'merchant.com.myapp',
|
|
8533
|
+
merchantName: 'My App',
|
|
8534
|
+
merchantCountryCode: 'US',
|
|
8535
|
+
});
|
|
6735
8536
|
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
height={300}
|
|
6762
|
-
stacked
|
|
6763
|
-
animate
|
|
6764
|
-
/>
|
|
6765
|
-
);
|
|
8537
|
+
const status = getPaymentStatus();
|
|
8538
|
+
console.log('Payment ready:', status.state === 'ready');
|
|
8539
|
+
}
|
|
8540
|
+
|
|
8541
|
+
// Check availability
|
|
8542
|
+
async function canPay() {
|
|
8543
|
+
const methods = await checkPaymentAvailability();
|
|
8544
|
+
return methods.some(m => m.isAvailable);
|
|
8545
|
+
}
|
|
8546
|
+
|
|
8547
|
+
// Process payment
|
|
8548
|
+
async function processPayment(clientSecret: string, totalCents: number) {
|
|
8549
|
+
try {
|
|
8550
|
+
const result = await confirmPayment({
|
|
8551
|
+
clientSecret,
|
|
8552
|
+
amount: { amount: totalCents, currencyCode: 'usd' },
|
|
8553
|
+
});
|
|
8554
|
+
return result;
|
|
8555
|
+
} catch (err) {
|
|
8556
|
+
const paymentErr = err as PaymentError;
|
|
8557
|
+
if (paymentErr.code === 'user_cancelled') {
|
|
8558
|
+
return null; // User cancelled \u2014 not an error
|
|
8559
|
+
}
|
|
8560
|
+
throw paymentErr;
|
|
8561
|
+
}
|
|
6766
8562
|
}
|
|
6767
8563
|
\`\`\`
|
|
6768
8564
|
|
|
6769
|
-
##
|
|
8565
|
+
## Platform-Conditional UI
|
|
6770
8566
|
|
|
6771
8567
|
\`\`\`tsx
|
|
6772
|
-
import
|
|
6773
|
-
import {
|
|
8568
|
+
import { usePayments } from '@idealyst/payments';
|
|
8569
|
+
import { Platform } from 'react-native';
|
|
6774
8570
|
|
|
6775
|
-
function
|
|
6776
|
-
const
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
8571
|
+
function PaymentButtons({ clientSecret }: { clientSecret: string }) {
|
|
8572
|
+
const {
|
|
8573
|
+
isApplePayAvailable,
|
|
8574
|
+
isGooglePayAvailable,
|
|
8575
|
+
confirmPayment,
|
|
8576
|
+
isProcessing,
|
|
8577
|
+
} = usePayments({
|
|
8578
|
+
config: {
|
|
8579
|
+
publishableKey: 'pk_test_xxx',
|
|
8580
|
+
merchantName: 'My Store',
|
|
8581
|
+
merchantCountryCode: 'US',
|
|
8582
|
+
},
|
|
8583
|
+
});
|
|
8584
|
+
|
|
8585
|
+
const handlePlatformPay = () =>
|
|
8586
|
+
confirmPayment({
|
|
8587
|
+
clientSecret,
|
|
8588
|
+
amount: { amount: 2499, currencyCode: 'usd' },
|
|
8589
|
+
});
|
|
6782
8590
|
|
|
6783
8591
|
return (
|
|
6784
|
-
<
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
8592
|
+
<View>
|
|
8593
|
+
{isApplePayAvailable && (
|
|
8594
|
+
<Button
|
|
8595
|
+
onPress={handlePlatformPay}
|
|
8596
|
+
disabled={isProcessing}
|
|
8597
|
+
iconName="apple"
|
|
8598
|
+
>
|
|
8599
|
+
Apple Pay
|
|
8600
|
+
</Button>
|
|
8601
|
+
)}
|
|
8602
|
+
|
|
8603
|
+
{isGooglePayAvailable && (
|
|
8604
|
+
<Button
|
|
8605
|
+
onPress={handlePlatformPay}
|
|
8606
|
+
disabled={isProcessing}
|
|
8607
|
+
iconName="google"
|
|
8608
|
+
>
|
|
8609
|
+
Google Pay
|
|
8610
|
+
</Button>
|
|
8611
|
+
)}
|
|
8612
|
+
|
|
8613
|
+
{/* Always show a card fallback */}
|
|
8614
|
+
<Button
|
|
8615
|
+
onPress={() => navigateToCardForm()}
|
|
8616
|
+
intent="secondary"
|
|
8617
|
+
>
|
|
8618
|
+
Pay with Card
|
|
8619
|
+
</Button>
|
|
8620
|
+
</View>
|
|
6790
8621
|
);
|
|
6791
8622
|
}
|
|
6792
8623
|
\`\`\`
|
|
8624
|
+
|
|
8625
|
+
## Best Practices
|
|
8626
|
+
|
|
8627
|
+
1. **Server-side PaymentIntent** \u2014 Always create PaymentIntents on your server, never on the client
|
|
8628
|
+
2. **Handle cancellation** \u2014 \`user_cancelled\` is not an error, don't show error UI for it
|
|
8629
|
+
3. **Test environment** \u2014 Set \`testEnvironment: true\` during development for Google Pay
|
|
8630
|
+
4. **Apple Pay merchant ID** \u2014 Requires Apple Developer Program and Xcode capability setup
|
|
8631
|
+
5. **Amounts in cents** \u2014 All amounts are in the smallest currency unit (1099 = $10.99)
|
|
8632
|
+
6. **Web fallback** \u2014 On web, use \`@stripe/react-stripe-js\` (Stripe Elements) directly
|
|
8633
|
+
7. **3D Secure** \u2014 Set \`urlScheme\` in config for 3D Secure / bank redirect flows on native
|
|
8634
|
+
8. **Error handling** \u2014 Always wrap \`confirmPayment\` / \`createPaymentMethod\` in try/catch
|
|
6793
8635
|
`
|
|
6794
8636
|
};
|
|
6795
8637
|
|
|
@@ -7710,6 +9552,156 @@ function UploadForm() {
|
|
|
7710
9552
|
"Use customMimeTypes for specific formats like 'application/pdf'"
|
|
7711
9553
|
],
|
|
7712
9554
|
relatedPackages: ["components", "camera", "storage"]
|
|
9555
|
+
},
|
|
9556
|
+
clipboard: {
|
|
9557
|
+
name: "Clipboard",
|
|
9558
|
+
npmName: "@idealyst/clipboard",
|
|
9559
|
+
description: "Cross-platform clipboard and OTP autofill for React and React Native. Copy/paste text and auto-detect SMS verification codes on mobile.",
|
|
9560
|
+
category: "utility",
|
|
9561
|
+
platforms: ["web", "native"],
|
|
9562
|
+
documentationStatus: "full",
|
|
9563
|
+
installation: "yarn add @idealyst/clipboard",
|
|
9564
|
+
peerDependencies: [
|
|
9565
|
+
"@react-native-clipboard/clipboard (native)",
|
|
9566
|
+
"react-native-otp-verify (Android OTP, optional)"
|
|
9567
|
+
],
|
|
9568
|
+
features: [
|
|
9569
|
+
"Cross-platform copy/paste with async API",
|
|
9570
|
+
"Android SMS OTP auto-read via SMS Retriever API (no permissions)",
|
|
9571
|
+
"iOS OTP keyboard autofill via TextInput props",
|
|
9572
|
+
"Clipboard change listeners",
|
|
9573
|
+
"Graceful degradation when native modules missing"
|
|
9574
|
+
],
|
|
9575
|
+
quickStart: `import { clipboard } from '@idealyst/clipboard';
|
|
9576
|
+
|
|
9577
|
+
// Copy text
|
|
9578
|
+
await clipboard.copy('Hello, world!');
|
|
9579
|
+
|
|
9580
|
+
// Paste text
|
|
9581
|
+
const text = await clipboard.paste();
|
|
9582
|
+
|
|
9583
|
+
// OTP auto-fill (mobile)
|
|
9584
|
+
import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
9585
|
+
|
|
9586
|
+
function OTPScreen() {
|
|
9587
|
+
const { code, startListening } = useOTPAutoFill({
|
|
9588
|
+
codeLength: 6,
|
|
9589
|
+
onCodeReceived: (otp) => verifyOTP(otp),
|
|
9590
|
+
});
|
|
9591
|
+
|
|
9592
|
+
useEffect(() => { startListening(); }, []);
|
|
9593
|
+
|
|
9594
|
+
return <TextInput value={code ?? ''} {...OTP_INPUT_PROPS} />;
|
|
9595
|
+
}`,
|
|
9596
|
+
apiHighlights: [
|
|
9597
|
+
"clipboard.copy(text) - Copy text to clipboard",
|
|
9598
|
+
"clipboard.paste() - Read text from clipboard",
|
|
9599
|
+
"clipboard.hasText() - Check if clipboard has text",
|
|
9600
|
+
"clipboard.addListener(fn) - Listen for copy events",
|
|
9601
|
+
"useOTPAutoFill({ codeLength, onCodeReceived }) - Auto-detect SMS OTP codes (Android)",
|
|
9602
|
+
"OTP_INPUT_PROPS - TextInput props for iOS OTP keyboard autofill"
|
|
9603
|
+
],
|
|
9604
|
+
relatedPackages: ["storage", "components"]
|
|
9605
|
+
},
|
|
9606
|
+
biometrics: {
|
|
9607
|
+
name: "Biometrics",
|
|
9608
|
+
npmName: "@idealyst/biometrics",
|
|
9609
|
+
description: "Cross-platform biometric authentication and passkeys (WebAuthn/FIDO2) for React and React Native. FaceID, TouchID, fingerprint, iris, and passwordless login.",
|
|
9610
|
+
category: "auth",
|
|
9611
|
+
platforms: ["web", "native"],
|
|
9612
|
+
documentationStatus: "full",
|
|
9613
|
+
installation: "yarn add @idealyst/biometrics",
|
|
9614
|
+
peerDependencies: [
|
|
9615
|
+
"expo-local-authentication (native biometrics)",
|
|
9616
|
+
"react-native-passkeys (native passkeys, optional)"
|
|
9617
|
+
],
|
|
9618
|
+
features: [
|
|
9619
|
+
"Local biometric auth \u2014 FaceID, TouchID, fingerprint, iris",
|
|
9620
|
+
"Passkeys (WebAuthn/FIDO2) \u2014 passwordless login with cryptographic credentials",
|
|
9621
|
+
"Cross-platform \u2014 single API for web and React Native",
|
|
9622
|
+
"Web biometrics via WebAuthn userVerification",
|
|
9623
|
+
"Web passkeys via navigator.credentials",
|
|
9624
|
+
"Graceful degradation when native modules missing",
|
|
9625
|
+
"Base64url encoding helpers for WebAuthn data"
|
|
9626
|
+
],
|
|
9627
|
+
quickStart: `import { isBiometricAvailable, authenticate } from '@idealyst/biometrics';
|
|
9628
|
+
|
|
9629
|
+
// Check biometric availability
|
|
9630
|
+
const available = await isBiometricAvailable();
|
|
9631
|
+
|
|
9632
|
+
// Authenticate
|
|
9633
|
+
if (available) {
|
|
9634
|
+
const result = await authenticate({ promptMessage: 'Verify your identity' });
|
|
9635
|
+
if (result.success) {
|
|
9636
|
+
// Authenticated!
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
|
|
9640
|
+
// Passkeys
|
|
9641
|
+
import { isPasskeySupported, createPasskey, getPasskey } from '@idealyst/biometrics';
|
|
9642
|
+
|
|
9643
|
+
const credential = await createPasskey({
|
|
9644
|
+
challenge: serverChallenge,
|
|
9645
|
+
rp: { id: 'example.com', name: 'My App' },
|
|
9646
|
+
user: { id: userId, name: email, displayName: name },
|
|
9647
|
+
});`,
|
|
9648
|
+
apiHighlights: [
|
|
9649
|
+
"isBiometricAvailable() - Check biometric hardware and enrollment",
|
|
9650
|
+
"getBiometricTypes() - Get available biometric types",
|
|
9651
|
+
"getSecurityLevel() - Get device security level",
|
|
9652
|
+
"authenticate(options) - Prompt for biometric auth",
|
|
9653
|
+
"cancelAuthentication() - Cancel auth (Android)",
|
|
9654
|
+
"isPasskeySupported() - Check passkey support",
|
|
9655
|
+
"createPasskey(options) - Register a new passkey",
|
|
9656
|
+
"getPasskey(options) - Authenticate with passkey"
|
|
9657
|
+
],
|
|
9658
|
+
relatedPackages: ["oauth-client", "storage"]
|
|
9659
|
+
},
|
|
9660
|
+
payments: {
|
|
9661
|
+
name: "Payments",
|
|
9662
|
+
npmName: "@idealyst/payments",
|
|
9663
|
+
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.",
|
|
9664
|
+
category: "utility",
|
|
9665
|
+
platforms: ["web", "native"],
|
|
9666
|
+
documentationStatus: "full",
|
|
9667
|
+
installation: "yarn add @idealyst/payments @stripe/stripe-react-native",
|
|
9668
|
+
peerDependencies: [
|
|
9669
|
+
"@stripe/stripe-react-native (native)"
|
|
9670
|
+
],
|
|
9671
|
+
features: [
|
|
9672
|
+
"Apple Pay via Stripe Platform Pay (iOS)",
|
|
9673
|
+
"Google Pay via Stripe Platform Pay (Android)",
|
|
9674
|
+
"Flat function API + usePayments convenience hook",
|
|
9675
|
+
"PaymentIntent confirmation flow",
|
|
9676
|
+
"Payment method creation flow",
|
|
9677
|
+
"Normalized error handling across platforms",
|
|
9678
|
+
"Graceful degradation when Stripe SDK missing",
|
|
9679
|
+
"Web stub with guidance to Stripe Elements"
|
|
9680
|
+
],
|
|
9681
|
+
quickStart: `import { usePayments } from '@idealyst/payments';
|
|
9682
|
+
|
|
9683
|
+
const { isReady, isApplePayAvailable, confirmPayment } = usePayments({
|
|
9684
|
+
config: {
|
|
9685
|
+
publishableKey: 'pk_test_xxx',
|
|
9686
|
+
merchantIdentifier: 'merchant.com.myapp',
|
|
9687
|
+
merchantName: 'My App',
|
|
9688
|
+
merchantCountryCode: 'US',
|
|
9689
|
+
},
|
|
9690
|
+
});
|
|
9691
|
+
|
|
9692
|
+
const result = await confirmPayment({
|
|
9693
|
+
clientSecret: 'pi_xxx_secret_xxx',
|
|
9694
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
9695
|
+
});`,
|
|
9696
|
+
apiHighlights: [
|
|
9697
|
+
"initializePayments(config) - Initialize Stripe SDK",
|
|
9698
|
+
"checkPaymentAvailability() - Check Apple Pay / Google Pay / card",
|
|
9699
|
+
"confirmPayment(request) - Present sheet and confirm PaymentIntent",
|
|
9700
|
+
"createPaymentMethod(request) - Present sheet and create payment method",
|
|
9701
|
+
"getPaymentStatus() - Get current provider status",
|
|
9702
|
+
"usePayments(options?) - Convenience hook with state management"
|
|
9703
|
+
],
|
|
9704
|
+
relatedPackages: ["biometrics", "oauth-client"]
|
|
7713
9705
|
}
|
|
7714
9706
|
};
|
|
7715
9707
|
function getPackagesByCategory() {
|
|
@@ -19818,7 +21810,7 @@ function getStorageGuide(args) {
|
|
|
19818
21810
|
const guide = storageGuides[uri];
|
|
19819
21811
|
if (!guide) {
|
|
19820
21812
|
return textResponse(
|
|
19821
|
-
`Topic "${topic}" not found. Available topics: overview, api, examples`
|
|
21813
|
+
`Topic "${topic}" not found. Available topics: overview, api, examples, secure`
|
|
19822
21814
|
);
|
|
19823
21815
|
}
|
|
19824
21816
|
return textResponse(guide);
|
|
@@ -19944,6 +21936,39 @@ function getChartsGuide(args) {
|
|
|
19944
21936
|
}
|
|
19945
21937
|
return textResponse(guide);
|
|
19946
21938
|
}
|
|
21939
|
+
function getClipboardGuide(args) {
|
|
21940
|
+
const topic = args.topic;
|
|
21941
|
+
const uri = `idealyst://clipboard/${topic}`;
|
|
21942
|
+
const guide = clipboardGuides[uri];
|
|
21943
|
+
if (!guide) {
|
|
21944
|
+
return textResponse(
|
|
21945
|
+
`Topic "${topic}" not found. Available topics: overview, api, examples`
|
|
21946
|
+
);
|
|
21947
|
+
}
|
|
21948
|
+
return textResponse(guide);
|
|
21949
|
+
}
|
|
21950
|
+
function getBiometricsGuide(args) {
|
|
21951
|
+
const topic = args.topic;
|
|
21952
|
+
const uri = `idealyst://biometrics/${topic}`;
|
|
21953
|
+
const guide = biometricsGuides[uri];
|
|
21954
|
+
if (!guide) {
|
|
21955
|
+
return textResponse(
|
|
21956
|
+
`Topic "${topic}" not found. Available topics: overview, api, examples`
|
|
21957
|
+
);
|
|
21958
|
+
}
|
|
21959
|
+
return textResponse(guide);
|
|
21960
|
+
}
|
|
21961
|
+
function getPaymentsGuide(args) {
|
|
21962
|
+
const topic = args.topic;
|
|
21963
|
+
const uri = `idealyst://payments/${topic}`;
|
|
21964
|
+
const guide = paymentsGuides[uri];
|
|
21965
|
+
if (!guide) {
|
|
21966
|
+
return textResponse(
|
|
21967
|
+
`Topic "${topic}" not found. Available topics: overview, api, examples`
|
|
21968
|
+
);
|
|
21969
|
+
}
|
|
21970
|
+
return textResponse(guide);
|
|
21971
|
+
}
|
|
19947
21972
|
function listPackages(args = {}) {
|
|
19948
21973
|
const category = args.category;
|
|
19949
21974
|
if (category) {
|
|
@@ -20189,6 +22214,9 @@ var toolHandlers = {
|
|
|
20189
22214
|
get_markdown_guide: getMarkdownGuide,
|
|
20190
22215
|
get_config_guide: getConfigGuide,
|
|
20191
22216
|
get_charts_guide: getChartsGuide,
|
|
22217
|
+
get_clipboard_guide: getClipboardGuide,
|
|
22218
|
+
get_biometrics_guide: getBiometricsGuide,
|
|
22219
|
+
get_payments_guide: getPaymentsGuide,
|
|
20192
22220
|
list_packages: listPackages,
|
|
20193
22221
|
get_package_docs: getPackageDocs,
|
|
20194
22222
|
search_packages: searchPackages2,
|
|
@@ -20235,6 +22263,9 @@ export {
|
|
|
20235
22263
|
getMarkdownGuideDefinition,
|
|
20236
22264
|
getConfigGuideDefinition,
|
|
20237
22265
|
getChartsGuideDefinition,
|
|
22266
|
+
getClipboardGuideDefinition,
|
|
22267
|
+
getBiometricsGuideDefinition,
|
|
22268
|
+
getPaymentsGuideDefinition,
|
|
20238
22269
|
listPackagesDefinition,
|
|
20239
22270
|
getPackageDocsDefinition,
|
|
20240
22271
|
searchPackagesDefinition,
|
|
@@ -20271,6 +22302,9 @@ export {
|
|
|
20271
22302
|
getMarkdownGuide,
|
|
20272
22303
|
getConfigGuide,
|
|
20273
22304
|
getChartsGuide,
|
|
22305
|
+
getClipboardGuide,
|
|
22306
|
+
getBiometricsGuide,
|
|
22307
|
+
getPaymentsGuide,
|
|
20274
22308
|
listPackages,
|
|
20275
22309
|
getPackageDocs,
|
|
20276
22310
|
searchPackages2 as searchPackages,
|
|
@@ -20282,4 +22316,4 @@ export {
|
|
|
20282
22316
|
toolHandlers,
|
|
20283
22317
|
callTool
|
|
20284
22318
|
};
|
|
20285
|
-
//# sourceMappingURL=chunk-
|
|
22319
|
+
//# sourceMappingURL=chunk-7WPOVADU.js.map
|