@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
package/dist/tools/index.cjs
CHANGED
|
@@ -36,12 +36,16 @@ __export(tools_exports, {
|
|
|
36
36
|
getAudioGuide: () => getAudioGuide,
|
|
37
37
|
getAudioGuideDefinition: () => getAudioGuideDefinition,
|
|
38
38
|
getAvailableComponents: () => getAvailableComponents,
|
|
39
|
+
getBiometricsGuide: () => getBiometricsGuide,
|
|
40
|
+
getBiometricsGuideDefinition: () => getBiometricsGuideDefinition,
|
|
39
41
|
getCameraGuide: () => getCameraGuide,
|
|
40
42
|
getCameraGuideDefinition: () => getCameraGuideDefinition,
|
|
41
43
|
getChartsGuide: () => getChartsGuide,
|
|
42
44
|
getChartsGuideDefinition: () => getChartsGuideDefinition,
|
|
43
45
|
getCliUsage: () => getCliUsage,
|
|
44
46
|
getCliUsageDefinition: () => getCliUsageDefinition,
|
|
47
|
+
getClipboardGuide: () => getClipboardGuide,
|
|
48
|
+
getClipboardGuideDefinition: () => getClipboardGuideDefinition,
|
|
45
49
|
getComponentDocs: () => getComponentDocs,
|
|
46
50
|
getComponentDocsDefinition: () => getComponentDocsDefinition,
|
|
47
51
|
getComponentExample: () => getComponentExample,
|
|
@@ -73,6 +77,8 @@ __export(tools_exports, {
|
|
|
73
77
|
getOauthClientGuideDefinition: () => getOauthClientGuideDefinition,
|
|
74
78
|
getPackageDocs: () => getPackageDocs,
|
|
75
79
|
getPackageDocsDefinition: () => getPackageDocsDefinition,
|
|
80
|
+
getPaymentsGuide: () => getPaymentsGuide,
|
|
81
|
+
getPaymentsGuideDefinition: () => getPaymentsGuideDefinition,
|
|
76
82
|
getRecipe: () => getRecipe,
|
|
77
83
|
getRecipeDefinition: () => getRecipeDefinition,
|
|
78
84
|
getRegistryThemeValues: () => getRegistryThemeValues,
|
|
@@ -288,8 +294,8 @@ var getStorageGuideDefinition = {
|
|
|
288
294
|
properties: {
|
|
289
295
|
topic: {
|
|
290
296
|
type: "string",
|
|
291
|
-
description: "Topic to get docs for: 'overview', 'api', 'examples'",
|
|
292
|
-
enum: ["overview", "api", "examples"]
|
|
297
|
+
description: "Topic to get docs for: 'overview', 'api', 'examples', 'secure'",
|
|
298
|
+
enum: ["overview", "api", "examples", "secure"]
|
|
293
299
|
}
|
|
294
300
|
},
|
|
295
301
|
required: ["topic"]
|
|
@@ -460,6 +466,51 @@ var getChartsGuideDefinition = {
|
|
|
460
466
|
required: ["topic"]
|
|
461
467
|
}
|
|
462
468
|
};
|
|
469
|
+
var getClipboardGuideDefinition = {
|
|
470
|
+
name: "get_clipboard_guide",
|
|
471
|
+
description: "Get documentation for @idealyst/clipboard cross-platform clipboard and OTP autofill package. Covers copy/paste API, useOTPAutoFill hook, and examples.",
|
|
472
|
+
inputSchema: {
|
|
473
|
+
type: "object",
|
|
474
|
+
properties: {
|
|
475
|
+
topic: {
|
|
476
|
+
type: "string",
|
|
477
|
+
description: "Topic to get docs for: 'overview', 'api', 'examples'",
|
|
478
|
+
enum: ["overview", "api", "examples"]
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
required: ["topic"]
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
var getBiometricsGuideDefinition = {
|
|
485
|
+
name: "get_biometrics_guide",
|
|
486
|
+
description: "Get documentation for @idealyst/biometrics cross-platform biometric authentication and passkeys (WebAuthn/FIDO2) package. Covers local biometric auth, passkey registration/login, and examples.",
|
|
487
|
+
inputSchema: {
|
|
488
|
+
type: "object",
|
|
489
|
+
properties: {
|
|
490
|
+
topic: {
|
|
491
|
+
type: "string",
|
|
492
|
+
description: "Topic to get docs for: 'overview', 'api', 'examples'",
|
|
493
|
+
enum: ["overview", "api", "examples"]
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
required: ["topic"]
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
var getPaymentsGuideDefinition = {
|
|
500
|
+
name: "get_payments_guide",
|
|
501
|
+
description: "Get documentation for @idealyst/payments cross-platform payment provider package. Covers Apple Pay, Google Pay, Stripe Platform Pay, usePayments hook, and examples.",
|
|
502
|
+
inputSchema: {
|
|
503
|
+
type: "object",
|
|
504
|
+
properties: {
|
|
505
|
+
topic: {
|
|
506
|
+
type: "string",
|
|
507
|
+
description: "Topic to get docs for: 'overview', 'api', 'examples'",
|
|
508
|
+
enum: ["overview", "api", "examples"]
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
required: ["topic"]
|
|
512
|
+
}
|
|
513
|
+
};
|
|
463
514
|
var listPackagesDefinition = {
|
|
464
515
|
name: "list_packages",
|
|
465
516
|
description: "List all available Idealyst packages with descriptions, categories, and documentation status. Use this to discover what packages are available in the framework.",
|
|
@@ -614,6 +665,9 @@ var toolDefinitions = [
|
|
|
614
665
|
getMarkdownGuideDefinition,
|
|
615
666
|
getConfigGuideDefinition,
|
|
616
667
|
getChartsGuideDefinition,
|
|
668
|
+
getClipboardGuideDefinition,
|
|
669
|
+
getBiometricsGuideDefinition,
|
|
670
|
+
getPaymentsGuideDefinition,
|
|
617
671
|
// Package tools
|
|
618
672
|
listPackagesDefinition,
|
|
619
673
|
getPackageDocsDefinition,
|
|
@@ -2436,6 +2490,7 @@ Cross-platform storage solution for React and React Native applications. Provide
|
|
|
2436
2490
|
- **React Native** - Uses MMKV for high-performance storage
|
|
2437
2491
|
- **Web** - Uses localStorage with proper error handling
|
|
2438
2492
|
- **TypeScript** - Full type safety and IntelliSense support
|
|
2493
|
+
- **Secure Storage** - Optional encrypted storage via \`createSecureStorage()\` (Keychain on native, Web Crypto on web)
|
|
2439
2494
|
|
|
2440
2495
|
## Installation
|
|
2441
2496
|
|
|
@@ -2839,6 +2894,127 @@ async function safeStorageOperation() {
|
|
|
2839
2894
|
6. **Data Size** - Keep stored objects reasonably sized
|
|
2840
2895
|
7. **Cleanup** - Periodically clean up unused data
|
|
2841
2896
|
8. **Type Safety** - Create typed wrapper functions for better TypeScript support
|
|
2897
|
+
9. **Use Secure Storage for Secrets** - Use \`createSecureStorage()\` for auth tokens, API keys, and sensitive data
|
|
2898
|
+
`,
|
|
2899
|
+
"idealyst://storage/secure": `# Secure Storage
|
|
2900
|
+
|
|
2901
|
+
Encrypted storage for sensitive data like auth tokens, API keys, and secrets. Uses the same \`IStorage\` interface as regular storage \u2014 drop-in replacement.
|
|
2902
|
+
|
|
2903
|
+
## Installation
|
|
2904
|
+
|
|
2905
|
+
\`\`\`bash
|
|
2906
|
+
yarn add @idealyst/storage
|
|
2907
|
+
|
|
2908
|
+
# React Native also needs (for secure storage):
|
|
2909
|
+
yarn add react-native-keychain react-native-mmkv
|
|
2910
|
+
cd ios && pod install
|
|
2911
|
+
\`\`\`
|
|
2912
|
+
|
|
2913
|
+
## Quick Start
|
|
2914
|
+
|
|
2915
|
+
\`\`\`tsx
|
|
2916
|
+
import { createSecureStorage } from '@idealyst/storage';
|
|
2917
|
+
|
|
2918
|
+
// Create a secure storage instance
|
|
2919
|
+
const secureStorage = createSecureStorage();
|
|
2920
|
+
|
|
2921
|
+
// Same API as regular storage
|
|
2922
|
+
await secureStorage.setItem('authToken', 'eyJhbGciOiJIUzI1NiIs...');
|
|
2923
|
+
const token = await secureStorage.getItem('authToken');
|
|
2924
|
+
await secureStorage.removeItem('authToken');
|
|
2925
|
+
await secureStorage.clear();
|
|
2926
|
+
const keys = await secureStorage.getAllKeys();
|
|
2927
|
+
|
|
2928
|
+
// Listeners work too
|
|
2929
|
+
const unsubscribe = secureStorage.addListener((key, value) => {
|
|
2930
|
+
console.log('Secure storage changed:', key);
|
|
2931
|
+
});
|
|
2932
|
+
\`\`\`
|
|
2933
|
+
|
|
2934
|
+
## Options
|
|
2935
|
+
|
|
2936
|
+
\`\`\`tsx
|
|
2937
|
+
import { createSecureStorage, SecureStorageOptions } from '@idealyst/storage';
|
|
2938
|
+
|
|
2939
|
+
const secureStorage = createSecureStorage({
|
|
2940
|
+
prefix: 'myapp', // Namespace for keys (default: 'secure')
|
|
2941
|
+
});
|
|
2942
|
+
\`\`\`
|
|
2943
|
+
|
|
2944
|
+
The \`prefix\` option controls:
|
|
2945
|
+
- **Native**: Keychain service name and MMKV instance ID
|
|
2946
|
+
- **Web**: localStorage key prefix and IndexedDB key name
|
|
2947
|
+
|
|
2948
|
+
Use different prefixes to create isolated secure storage instances.
|
|
2949
|
+
|
|
2950
|
+
## How It Works
|
|
2951
|
+
|
|
2952
|
+
### React Native
|
|
2953
|
+
1. A random 16-byte encryption key is generated on first use
|
|
2954
|
+
2. The key is stored in the **iOS Keychain** / **Android Keystore** (hardware-backed)
|
|
2955
|
+
3. An encrypted MMKV instance is created using that key
|
|
2956
|
+
4. All data is encrypted at rest by MMKV's native AES encryption
|
|
2957
|
+
5. Keychain accessibility is set to \`WHEN_UNLOCKED_THIS_DEVICE_ONLY\` (not backed up, only accessible when device is unlocked)
|
|
2958
|
+
|
|
2959
|
+
### Web
|
|
2960
|
+
1. A non-extractable AES-256-GCM \`CryptoKey\` is generated on first use
|
|
2961
|
+
2. The key is stored in **IndexedDB** (non-extractable \u2014 cannot be read as raw bytes)
|
|
2962
|
+
3. Each value is encrypted with a unique random IV before storing in localStorage
|
|
2963
|
+
4. Requires a **secure context** (HTTPS) for \`crypto.subtle\` access
|
|
2964
|
+
|
|
2965
|
+
## Usage Example: Secure Auth Service
|
|
2966
|
+
|
|
2967
|
+
\`\`\`tsx
|
|
2968
|
+
import { createSecureStorage } from '@idealyst/storage';
|
|
2969
|
+
|
|
2970
|
+
const secureStorage = createSecureStorage({ prefix: 'auth' });
|
|
2971
|
+
|
|
2972
|
+
interface AuthTokens {
|
|
2973
|
+
accessToken: string;
|
|
2974
|
+
refreshToken: string;
|
|
2975
|
+
expiresAt: number;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
class SecureAuthService {
|
|
2979
|
+
static async saveTokens(tokens: AuthTokens) {
|
|
2980
|
+
await secureStorage.setItem('tokens', JSON.stringify(tokens));
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
static async getTokens(): Promise<AuthTokens | null> {
|
|
2984
|
+
const data = await secureStorage.getItem('tokens');
|
|
2985
|
+
return data ? JSON.parse(data) as AuthTokens : null;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
static async clearTokens() {
|
|
2989
|
+
await secureStorage.removeItem('tokens');
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
static async saveApiKey(key: string) {
|
|
2993
|
+
await secureStorage.setItem('apiKey', key);
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
static async getApiKey(): Promise<string | null> {
|
|
2997
|
+
return secureStorage.getItem('apiKey');
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
\`\`\`
|
|
3001
|
+
|
|
3002
|
+
## When to Use Secure vs Regular Storage
|
|
3003
|
+
|
|
3004
|
+
| Data Type | Use |
|
|
3005
|
+
|-----------|-----|
|
|
3006
|
+
| Auth tokens, refresh tokens | \`createSecureStorage()\` |
|
|
3007
|
+
| API keys, client secrets | \`createSecureStorage()\` |
|
|
3008
|
+
| User preferences, theme | \`storage\` (regular) |
|
|
3009
|
+
| Cache data | \`storage\` (regular) |
|
|
3010
|
+
| Session IDs | \`createSecureStorage()\` |
|
|
3011
|
+
| Language preference | \`storage\` (regular) |
|
|
3012
|
+
|
|
3013
|
+
## Platform Requirements
|
|
3014
|
+
|
|
3015
|
+
- **React Native**: Requires \`react-native-keychain\` (>=9.0.0) and \`react-native-mmkv\` (>=4.0.0)
|
|
3016
|
+
- **Web**: Requires secure context (HTTPS) and IndexedDB support
|
|
3017
|
+
- **Web limitation**: IndexedDB may not be available in some private browsing modes
|
|
2842
3018
|
`
|
|
2843
3019
|
};
|
|
2844
3020
|
|
|
@@ -2870,7 +3046,8 @@ yarn add @idealyst/audio
|
|
|
2870
3046
|
1. **PCM Streaming** \u2014 Audio data is delivered as \`PCMData\` chunks via callbacks, not as files
|
|
2871
3047
|
2. **Audio Session** \u2014 On iOS/Android, configure the audio session category before recording/playback
|
|
2872
3048
|
3. **Audio Profiles** \u2014 Pre-configured \`AudioConfig\` presets: \`speech\`, \`highQuality\`, \`studio\`, \`phone\`
|
|
2873
|
-
4. **Session Presets** \u2014 Pre-configured \`AudioSessionConfig\` presets: \`playback\`, \`record\`, \`voiceChat\`, \`ambient\`, \`default\`
|
|
3049
|
+
4. **Session Presets** \u2014 Pre-configured \`AudioSessionConfig\` presets: \`playback\`, \`record\`, \`voiceChat\`, \`ambient\`, \`default\`, \`backgroundRecord\`
|
|
3050
|
+
5. **Background Recording** \u2014 \`useBackgroundRecorder\` hook for recording that continues when the app is backgrounded (iOS/Android). Requires app-level native entitlements.
|
|
2874
3051
|
|
|
2875
3052
|
## Exports
|
|
2876
3053
|
|
|
@@ -2879,10 +3056,14 @@ import {
|
|
|
2879
3056
|
useRecorder,
|
|
2880
3057
|
usePlayer,
|
|
2881
3058
|
useAudio,
|
|
3059
|
+
useBackgroundRecorder,
|
|
2882
3060
|
AUDIO_PROFILES,
|
|
2883
3061
|
SESSION_PRESETS,
|
|
2884
3062
|
} from '@idealyst/audio';
|
|
2885
|
-
import type {
|
|
3063
|
+
import type {
|
|
3064
|
+
PCMData, AudioConfig, AudioLevel,
|
|
3065
|
+
BackgroundRecorderStatus, BackgroundLifecycleInfo,
|
|
3066
|
+
} from '@idealyst/audio';
|
|
2886
3067
|
\`\`\`
|
|
2887
3068
|
`,
|
|
2888
3069
|
"idealyst://audio/api": `# @idealyst/audio \u2014 API Reference
|
|
@@ -2992,6 +3173,84 @@ interface UseAudioOptions {
|
|
|
2992
3173
|
|
|
2993
3174
|
---
|
|
2994
3175
|
|
|
3176
|
+
### useBackgroundRecorder(options?)
|
|
3177
|
+
|
|
3178
|
+
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).
|
|
3179
|
+
|
|
3180
|
+
> **Requires app-level native configuration** \u2014 see "Background Recording Setup" below.
|
|
3181
|
+
|
|
3182
|
+
\`\`\`typescript
|
|
3183
|
+
interface UseBackgroundRecorderOptions {
|
|
3184
|
+
config?: Partial<AudioConfig>; // Audio config
|
|
3185
|
+
session?: Partial<AudioSessionConfig>; // Session config (default: SESSION_PRESETS.backgroundRecord)
|
|
3186
|
+
autoRequestPermission?: boolean; // Auto-request mic permission on mount
|
|
3187
|
+
levelUpdateInterval?: number; // Level update interval in ms (default: 100)
|
|
3188
|
+
maxBackgroundDuration?: number; // Max background recording time in ms (undefined = no limit)
|
|
3189
|
+
autoConfigureSession?: boolean; // Auto-configure session for background (default: true)
|
|
3190
|
+
onLifecycleEvent?: BackgroundLifecycleCallback; // Lifecycle event callback
|
|
3191
|
+
}
|
|
3192
|
+
\`\`\`
|
|
3193
|
+
|
|
3194
|
+
**Returns \`UseBackgroundRecorderResult\`:**
|
|
3195
|
+
|
|
3196
|
+
All properties from \`useRecorder\`, plus:
|
|
3197
|
+
|
|
3198
|
+
| Property | Type | Description |
|
|
3199
|
+
|----------|------|-------------|
|
|
3200
|
+
| isInBackground | boolean | Whether the app is currently backgrounded |
|
|
3201
|
+
| wasInterrupted | boolean | Whether recording was interrupted (phone call, Siri, etc.) |
|
|
3202
|
+
| backgroundDuration | number | Total time spent recording in background (ms) |
|
|
3203
|
+
| appState | AppStateStatus | Current app state (\`'active' \\| 'background' \\| 'inactive'\`) |
|
|
3204
|
+
|
|
3205
|
+
**Lifecycle events** (via \`onLifecycleEvent\`):
|
|
3206
|
+
|
|
3207
|
+
| Event | When | Extra fields |
|
|
3208
|
+
|-------|------|-------------|
|
|
3209
|
+
| \`'backgrounded'\` | App enters background while recording | \u2014 |
|
|
3210
|
+
| \`'foregrounded'\` | App returns to foreground while recording | \`backgroundDuration\` |
|
|
3211
|
+
| \`'interrupted'\` | OS interrupts recording (phone call, Siri) | \u2014 |
|
|
3212
|
+
| \`'interruptionEnded'\` | OS interruption ends | \`shouldResume\` |
|
|
3213
|
+
| \`'maxDurationReached'\` | Background recording hit \`maxBackgroundDuration\` | \`backgroundDuration\` |
|
|
3214
|
+
| \`'stopped'\` | Recording stopped while in background | \`backgroundDuration\` |
|
|
3215
|
+
|
|
3216
|
+
> **Note:** Interruptions use notify-only \u2014 the hook does NOT auto-resume. The consumer decides via the \`shouldResume\` flag.
|
|
3217
|
+
|
|
3218
|
+
#### Background Recording Setup
|
|
3219
|
+
|
|
3220
|
+
The OS will not allow background recording without app-level entitlements:
|
|
3221
|
+
|
|
3222
|
+
**iOS** \u2014 \`Info.plist\`:
|
|
3223
|
+
\`\`\`xml
|
|
3224
|
+
<key>UIBackgroundModes</key>
|
|
3225
|
+
<array><string>audio</string></array>
|
|
3226
|
+
\`\`\`
|
|
3227
|
+
|
|
3228
|
+
**Android** \u2014 \`AndroidManifest.xml\`:
|
|
3229
|
+
\`\`\`xml
|
|
3230
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
3231
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
|
3232
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
3233
|
+
<service
|
|
3234
|
+
android:name="com.swmansion.audioapi.system.CentralizedForegroundService"
|
|
3235
|
+
android:foregroundServiceType="microphone" />
|
|
3236
|
+
\`\`\`
|
|
3237
|
+
|
|
3238
|
+
**Expo** \u2014 \`app.json\` plugin:
|
|
3239
|
+
\`\`\`json
|
|
3240
|
+
["react-native-audio-api", {
|
|
3241
|
+
"iosBackgroundMode": true,
|
|
3242
|
+
"androidForegroundService": true,
|
|
3243
|
+
"androidFSTypes": ["microphone"],
|
|
3244
|
+
"androidPermissions": [
|
|
3245
|
+
"android.permission.FOREGROUND_SERVICE",
|
|
3246
|
+
"android.permission.FOREGROUND_SERVICE_MICROPHONE",
|
|
3247
|
+
"android.permission.RECORD_AUDIO"
|
|
3248
|
+
]
|
|
3249
|
+
}]
|
|
3250
|
+
\`\`\`
|
|
3251
|
+
|
|
3252
|
+
---
|
|
3253
|
+
|
|
2995
3254
|
## Types
|
|
2996
3255
|
|
|
2997
3256
|
### AudioConfig
|
|
@@ -3053,6 +3312,7 @@ type AudioErrorCode =
|
|
|
3053
3312
|
| 'DEVICE_NOT_FOUND' | 'DEVICE_IN_USE' | 'NOT_SUPPORTED'
|
|
3054
3313
|
| 'SOURCE_NOT_FOUND' | 'FORMAT_NOT_SUPPORTED' | 'DECODE_ERROR' | 'PLAYBACK_ERROR' | 'BUFFER_UNDERRUN'
|
|
3055
3314
|
| 'RECORDING_ERROR'
|
|
3315
|
+
| 'BACKGROUND_NOT_SUPPORTED' | 'BACKGROUND_MAX_DURATION'
|
|
3056
3316
|
| 'INITIALIZATION_FAILED' | 'INVALID_STATE' | 'INVALID_CONFIG' | 'UNKNOWN';
|
|
3057
3317
|
|
|
3058
3318
|
interface AudioError {
|
|
@@ -3081,11 +3341,12 @@ const AUDIO_PROFILES: AudioProfiles = {
|
|
|
3081
3341
|
|
|
3082
3342
|
\`\`\`typescript
|
|
3083
3343
|
const SESSION_PRESETS: SessionPresets = {
|
|
3084
|
-
playback:
|
|
3085
|
-
record:
|
|
3086
|
-
voiceChat:
|
|
3087
|
-
ambient:
|
|
3088
|
-
default:
|
|
3344
|
+
playback: { category: 'playback', mode: 'default' },
|
|
3345
|
+
record: { category: 'record', mode: 'default' },
|
|
3346
|
+
voiceChat: { category: 'playAndRecord', mode: 'voiceChat', categoryOptions: ['allowBluetooth', 'defaultToSpeaker'] },
|
|
3347
|
+
ambient: { category: 'ambient', mode: 'default' },
|
|
3348
|
+
default: { category: 'soloAmbient', mode: 'default' },
|
|
3349
|
+
backgroundRecord: { category: 'playAndRecord', mode: 'spokenAudio', categoryOptions: ['defaultToSpeaker', 'allowBluetooth', 'allowBluetoothA2DP', 'mixWithOthers'] },
|
|
3089
3350
|
};
|
|
3090
3351
|
\`\`\`
|
|
3091
3352
|
`,
|
|
@@ -3258,6 +3519,77 @@ function VoiceChatScreen() {
|
|
|
3258
3519
|
}
|
|
3259
3520
|
\`\`\`
|
|
3260
3521
|
|
|
3522
|
+
## Background Recording for Transcription
|
|
3523
|
+
|
|
3524
|
+
\`\`\`tsx
|
|
3525
|
+
import React, { useEffect } from 'react';
|
|
3526
|
+
import { View, Button, Text } from '@idealyst/components';
|
|
3527
|
+
import { useBackgroundRecorder, AUDIO_PROFILES } from '@idealyst/audio';
|
|
3528
|
+
import type { PCMData, BackgroundLifecycleInfo } from '@idealyst/audio';
|
|
3529
|
+
|
|
3530
|
+
function BackgroundTranscriber() {
|
|
3531
|
+
const recorder = useBackgroundRecorder({
|
|
3532
|
+
config: AUDIO_PROFILES.speech,
|
|
3533
|
+
maxBackgroundDuration: 5 * 60 * 1000, // 5 min max in background
|
|
3534
|
+
onLifecycleEvent: (info: BackgroundLifecycleInfo) => {
|
|
3535
|
+
switch (info.event) {
|
|
3536
|
+
case 'backgrounded':
|
|
3537
|
+
console.log('Recording continues in background');
|
|
3538
|
+
break;
|
|
3539
|
+
case 'foregrounded':
|
|
3540
|
+
console.log(\`Back from background after \${info.backgroundDuration}ms\`);
|
|
3541
|
+
break;
|
|
3542
|
+
case 'interrupted':
|
|
3543
|
+
console.log('Recording interrupted (phone call?)');
|
|
3544
|
+
break;
|
|
3545
|
+
case 'interruptionEnded':
|
|
3546
|
+
if (info.shouldResume) {
|
|
3547
|
+
recorder.resume(); // Consumer decides whether to resume
|
|
3548
|
+
}
|
|
3549
|
+
break;
|
|
3550
|
+
case 'maxDurationReached':
|
|
3551
|
+
console.log('Max background duration reached');
|
|
3552
|
+
break;
|
|
3553
|
+
}
|
|
3554
|
+
},
|
|
3555
|
+
});
|
|
3556
|
+
|
|
3557
|
+
// Stream PCM chunks to your speech-to-text service
|
|
3558
|
+
useEffect(() => {
|
|
3559
|
+
const unsub = recorder.subscribeToData((pcm: PCMData) => {
|
|
3560
|
+
// Send to STT API (e.g., Whisper, Deepgram)
|
|
3561
|
+
sendToTranscriptionService(pcm.toBase64());
|
|
3562
|
+
});
|
|
3563
|
+
return unsub;
|
|
3564
|
+
}, [recorder.subscribeToData]);
|
|
3565
|
+
|
|
3566
|
+
const handleToggle = async () => {
|
|
3567
|
+
if (recorder.isRecording) {
|
|
3568
|
+
await recorder.stop();
|
|
3569
|
+
} else {
|
|
3570
|
+
await recorder.start();
|
|
3571
|
+
}
|
|
3572
|
+
};
|
|
3573
|
+
|
|
3574
|
+
return (
|
|
3575
|
+
<View padding="md" gap="md">
|
|
3576
|
+
<Button
|
|
3577
|
+
onPress={handleToggle}
|
|
3578
|
+
intent={recorder.isRecording ? 'error' : 'primary'}
|
|
3579
|
+
>
|
|
3580
|
+
{recorder.isRecording ? 'Stop' : 'Record'}
|
|
3581
|
+
</Button>
|
|
3582
|
+
<Text>Duration: {Math.round(recorder.duration / 1000)}s</Text>
|
|
3583
|
+
{recorder.isInBackground && <Text>Recording in background...</Text>}
|
|
3584
|
+
{recorder.wasInterrupted && <Text>Recording was interrupted</Text>}
|
|
3585
|
+
<Text>Background time: {Math.round(recorder.backgroundDuration / 1000)}s</Text>
|
|
3586
|
+
</View>
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
\`\`\`
|
|
3590
|
+
|
|
3591
|
+
> **Important:** Background recording requires native entitlements. See the \`useBackgroundRecorder\` API docs for iOS, Android, and Expo setup instructions.
|
|
3592
|
+
|
|
3261
3593
|
## Audio Level Visualization
|
|
3262
3594
|
|
|
3263
3595
|
\`\`\`tsx
|
|
@@ -6751,146 +7083,1662 @@ function SalesOverview() {
|
|
|
6751
7083
|
}
|
|
6752
7084
|
\`\`\`
|
|
6753
7085
|
|
|
6754
|
-
## Multi-Series Line Chart
|
|
7086
|
+
## Multi-Series Line Chart
|
|
7087
|
+
|
|
7088
|
+
\`\`\`tsx
|
|
7089
|
+
import React from 'react';
|
|
7090
|
+
import { LineChart } from '@idealyst/charts';
|
|
7091
|
+
import type { ChartDataSeries } from '@idealyst/charts';
|
|
7092
|
+
|
|
7093
|
+
// Each series has: id, name, data, color? \u2014 NO 'label' property
|
|
7094
|
+
const series: ChartDataSeries[] = [
|
|
7095
|
+
{
|
|
7096
|
+
id: 'product-a',
|
|
7097
|
+
name: 'Product A', // Use 'name' \u2014 NOT 'label'
|
|
7098
|
+
data: [
|
|
7099
|
+
{ x: 'Q1', y: 120 },
|
|
7100
|
+
{ x: 'Q2', y: 150 },
|
|
7101
|
+
{ x: 'Q3', y: 180 },
|
|
7102
|
+
{ x: 'Q4', y: 210 },
|
|
7103
|
+
],
|
|
7104
|
+
color: '#2196F3',
|
|
7105
|
+
},
|
|
7106
|
+
{
|
|
7107
|
+
id: 'product-b',
|
|
7108
|
+
name: 'Product B', // Use 'name' \u2014 NOT 'label'
|
|
7109
|
+
data: [
|
|
7110
|
+
{ x: 'Q1', y: 80 },
|
|
7111
|
+
{ x: 'Q2', y: 110 },
|
|
7112
|
+
{ x: 'Q3', y: 95 },
|
|
7113
|
+
{ x: 'Q4', y: 140 },
|
|
7114
|
+
],
|
|
7115
|
+
color: '#FF9800',
|
|
7116
|
+
},
|
|
7117
|
+
];
|
|
7118
|
+
|
|
7119
|
+
function ComparisonChart() {
|
|
7120
|
+
return (
|
|
7121
|
+
<LineChart
|
|
7122
|
+
data={series}
|
|
7123
|
+
height={350}
|
|
7124
|
+
curve="monotone"
|
|
7125
|
+
showDots
|
|
7126
|
+
animate
|
|
7127
|
+
/>
|
|
7128
|
+
);
|
|
7129
|
+
}
|
|
7130
|
+
\`\`\`
|
|
7131
|
+
|
|
7132
|
+
## Bar Chart
|
|
7133
|
+
|
|
7134
|
+
\`\`\`tsx
|
|
7135
|
+
import React from 'react';
|
|
7136
|
+
import { View, Text } from '@idealyst/components';
|
|
7137
|
+
import { BarChart } from '@idealyst/charts';
|
|
7138
|
+
|
|
7139
|
+
const categories = [
|
|
7140
|
+
{ x: 'Electronics', y: 45 },
|
|
7141
|
+
{ x: 'Clothing', y: 32 },
|
|
7142
|
+
{ x: 'Books', y: 18 },
|
|
7143
|
+
{ x: 'Food', y: 56 },
|
|
7144
|
+
{ x: 'Sports', y: 28 },
|
|
7145
|
+
];
|
|
7146
|
+
|
|
7147
|
+
function CategoryBreakdown() {
|
|
7148
|
+
return (
|
|
7149
|
+
<View padding="md" gap="md">
|
|
7150
|
+
<Text typography="h6" weight="bold">Sales by Category</Text>
|
|
7151
|
+
<BarChart
|
|
7152
|
+
data={[{ id: 'units', name: 'Units Sold', data: categories }]}
|
|
7153
|
+
height={300}
|
|
7154
|
+
barRadius={4}
|
|
7155
|
+
animate
|
|
7156
|
+
yAxis={{ tickFormat: (value: number | string | Date) => \`\${value} units\` }}
|
|
7157
|
+
/>
|
|
7158
|
+
</View>
|
|
7159
|
+
);
|
|
7160
|
+
}
|
|
7161
|
+
\`\`\`
|
|
7162
|
+
|
|
7163
|
+
## Stacked Bar Chart
|
|
7164
|
+
|
|
7165
|
+
\`\`\`tsx
|
|
7166
|
+
import React from 'react';
|
|
7167
|
+
import { BarChart } from '@idealyst/charts';
|
|
7168
|
+
|
|
7169
|
+
function StackedBarExample() {
|
|
7170
|
+
return (
|
|
7171
|
+
<BarChart
|
|
7172
|
+
data={[
|
|
7173
|
+
{
|
|
7174
|
+
id: 'online',
|
|
7175
|
+
name: 'Online',
|
|
7176
|
+
data: [
|
|
7177
|
+
{ x: 'Q1', y: 100 },
|
|
7178
|
+
{ x: 'Q2', y: 120 },
|
|
7179
|
+
{ x: 'Q3', y: 90 },
|
|
7180
|
+
],
|
|
7181
|
+
color: '#4CAF50',
|
|
7182
|
+
},
|
|
7183
|
+
{
|
|
7184
|
+
id: 'in-store',
|
|
7185
|
+
name: 'In-Store',
|
|
7186
|
+
data: [
|
|
7187
|
+
{ x: 'Q1', y: 60 },
|
|
7188
|
+
{ x: 'Q2', y: 80 },
|
|
7189
|
+
{ x: 'Q3', y: 70 },
|
|
7190
|
+
],
|
|
7191
|
+
color: '#2196F3',
|
|
7192
|
+
},
|
|
7193
|
+
]}
|
|
7194
|
+
height={300}
|
|
7195
|
+
stacked
|
|
7196
|
+
animate
|
|
7197
|
+
/>
|
|
7198
|
+
);
|
|
7199
|
+
}
|
|
7200
|
+
\`\`\`
|
|
7201
|
+
|
|
7202
|
+
## Horizontal Bar Chart
|
|
7203
|
+
|
|
7204
|
+
\`\`\`tsx
|
|
7205
|
+
import React from 'react';
|
|
7206
|
+
import { BarChart } from '@idealyst/charts';
|
|
7207
|
+
|
|
7208
|
+
function HorizontalBarExample() {
|
|
7209
|
+
const data = [
|
|
7210
|
+
{ x: 'React', y: 85 },
|
|
7211
|
+
{ x: 'Vue', y: 62 },
|
|
7212
|
+
{ x: 'Angular', y: 45 },
|
|
7213
|
+
{ x: 'Svelte', y: 38 },
|
|
7214
|
+
];
|
|
7215
|
+
|
|
7216
|
+
return (
|
|
7217
|
+
<BarChart
|
|
7218
|
+
data={[{ id: 'popularity', name: 'Popularity', data }]}
|
|
7219
|
+
height={250}
|
|
7220
|
+
orientation="horizontal"
|
|
7221
|
+
animate
|
|
7222
|
+
/>
|
|
7223
|
+
);
|
|
7224
|
+
}
|
|
7225
|
+
\`\`\`
|
|
7226
|
+
`
|
|
7227
|
+
};
|
|
7228
|
+
|
|
7229
|
+
// src/data/clipboard-guides.ts
|
|
7230
|
+
var clipboardGuides = {
|
|
7231
|
+
"idealyst://clipboard/overview": `# @idealyst/clipboard Overview
|
|
7232
|
+
|
|
7233
|
+
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.
|
|
7234
|
+
|
|
7235
|
+
## Features
|
|
7236
|
+
|
|
7237
|
+
- **Cross-Platform Clipboard** - Copy and paste text on React Native and Web
|
|
7238
|
+
- **Simple API** - Async/await based with consistent interface
|
|
7239
|
+
- **React Native** - Uses @react-native-clipboard/clipboard
|
|
7240
|
+
- **Web** - Uses navigator.clipboard API
|
|
7241
|
+
- **OTP Auto-Fill (Android)** - Automatically reads OTP codes from SMS via SMS Retriever API (no permissions needed)
|
|
7242
|
+
- **OTP Auto-Fill (iOS)** - Provides TextInput props for native iOS keyboard OTP suggestion
|
|
7243
|
+
- **TypeScript** - Full type safety and IntelliSense support
|
|
7244
|
+
|
|
7245
|
+
## Installation
|
|
7246
|
+
|
|
7247
|
+
\`\`\`bash
|
|
7248
|
+
yarn add @idealyst/clipboard
|
|
7249
|
+
|
|
7250
|
+
# React Native also needs:
|
|
7251
|
+
yarn add @react-native-clipboard/clipboard
|
|
7252
|
+
cd ios && pod install
|
|
7253
|
+
|
|
7254
|
+
# For OTP autofill on Android (optional):
|
|
7255
|
+
yarn add react-native-otp-verify
|
|
7256
|
+
\`\`\`
|
|
7257
|
+
|
|
7258
|
+
## Quick Start
|
|
7259
|
+
|
|
7260
|
+
\`\`\`tsx
|
|
7261
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7262
|
+
|
|
7263
|
+
// Copy text
|
|
7264
|
+
await clipboard.copy('Hello, world!');
|
|
7265
|
+
|
|
7266
|
+
// Paste text
|
|
7267
|
+
const text = await clipboard.paste();
|
|
7268
|
+
|
|
7269
|
+
// Check if clipboard has text
|
|
7270
|
+
const hasText = await clipboard.hasText();
|
|
7271
|
+
\`\`\`
|
|
7272
|
+
|
|
7273
|
+
## OTP Auto-Fill Quick Start
|
|
7274
|
+
|
|
7275
|
+
\`\`\`tsx
|
|
7276
|
+
import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
7277
|
+
import { TextInput } from 'react-native';
|
|
7278
|
+
|
|
7279
|
+
function OTPScreen() {
|
|
7280
|
+
const { code, startListening, hash } = useOTPAutoFill({
|
|
7281
|
+
codeLength: 6,
|
|
7282
|
+
onCodeReceived: (otp) => verifyOTP(otp),
|
|
7283
|
+
});
|
|
7284
|
+
|
|
7285
|
+
useEffect(() => {
|
|
7286
|
+
startListening();
|
|
7287
|
+
}, []);
|
|
7288
|
+
|
|
7289
|
+
return (
|
|
7290
|
+
<TextInput
|
|
7291
|
+
value={code ?? ''}
|
|
7292
|
+
{...OTP_INPUT_PROPS}
|
|
7293
|
+
/>
|
|
7294
|
+
);
|
|
7295
|
+
}
|
|
7296
|
+
\`\`\`
|
|
7297
|
+
|
|
7298
|
+
## Import Options
|
|
7299
|
+
|
|
7300
|
+
\`\`\`tsx
|
|
7301
|
+
// Named import (recommended)
|
|
7302
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7303
|
+
|
|
7304
|
+
// Default import
|
|
7305
|
+
import clipboard from '@idealyst/clipboard';
|
|
7306
|
+
|
|
7307
|
+
// OTP hook and helpers
|
|
7308
|
+
import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
7309
|
+
\`\`\`
|
|
7310
|
+
|
|
7311
|
+
## Platform Details
|
|
7312
|
+
|
|
7313
|
+
- **React Native**: Uses \`@react-native-clipboard/clipboard\` for clipboard operations
|
|
7314
|
+
- **Web**: Uses \`navigator.clipboard\` API (requires secure context / HTTPS)
|
|
7315
|
+
- **OTP (Android)**: Uses SMS Retriever API via \`react-native-otp-verify\` \u2014 zero permissions
|
|
7316
|
+
- **OTP (iOS)**: Native keyboard autofill via \`textContentType="oneTimeCode"\`
|
|
7317
|
+
- **OTP (Web)**: No-op \u2014 returns null values and noop functions
|
|
7318
|
+
`,
|
|
7319
|
+
"idealyst://clipboard/api": `# Clipboard API Reference
|
|
7320
|
+
|
|
7321
|
+
Complete API reference for @idealyst/clipboard.
|
|
7322
|
+
|
|
7323
|
+
## clipboard.copy
|
|
7324
|
+
|
|
7325
|
+
Copy text to the system clipboard.
|
|
7326
|
+
|
|
7327
|
+
\`\`\`tsx
|
|
7328
|
+
await clipboard.copy(text: string): Promise<void>
|
|
7329
|
+
|
|
7330
|
+
// Examples
|
|
7331
|
+
await clipboard.copy('Hello, world!');
|
|
7332
|
+
await clipboard.copy(inviteCode);
|
|
7333
|
+
await clipboard.copy(JSON.stringify(data));
|
|
7334
|
+
\`\`\`
|
|
7335
|
+
|
|
7336
|
+
## clipboard.paste
|
|
7337
|
+
|
|
7338
|
+
Read text from the system clipboard.
|
|
7339
|
+
|
|
7340
|
+
\`\`\`tsx
|
|
7341
|
+
await clipboard.paste(): Promise<string>
|
|
7342
|
+
|
|
7343
|
+
// Examples
|
|
7344
|
+
const text = await clipboard.paste();
|
|
7345
|
+
const url = await clipboard.paste();
|
|
7346
|
+
\`\`\`
|
|
7347
|
+
|
|
7348
|
+
## clipboard.hasText
|
|
7349
|
+
|
|
7350
|
+
Check if the clipboard contains text content.
|
|
7351
|
+
|
|
7352
|
+
\`\`\`tsx
|
|
7353
|
+
await clipboard.hasText(): Promise<boolean>
|
|
7354
|
+
|
|
7355
|
+
// Example
|
|
7356
|
+
const canPaste = await clipboard.hasText();
|
|
7357
|
+
if (canPaste) {
|
|
7358
|
+
const text = await clipboard.paste();
|
|
7359
|
+
}
|
|
7360
|
+
\`\`\`
|
|
7361
|
+
|
|
7362
|
+
## clipboard.addListener
|
|
7363
|
+
|
|
7364
|
+
Listen for copy events (triggered when \`clipboard.copy()\` is called).
|
|
7365
|
+
|
|
7366
|
+
\`\`\`tsx
|
|
7367
|
+
const unsubscribe = clipboard.addListener((content: string) => {
|
|
7368
|
+
console.log('Copied:', content);
|
|
7369
|
+
});
|
|
7370
|
+
|
|
7371
|
+
// Later, unsubscribe
|
|
7372
|
+
unsubscribe();
|
|
7373
|
+
\`\`\`
|
|
7374
|
+
|
|
7375
|
+
---
|
|
7376
|
+
|
|
7377
|
+
## useOTPAutoFill
|
|
7378
|
+
|
|
7379
|
+
React hook for automatic OTP code detection from SMS on mobile.
|
|
7380
|
+
|
|
7381
|
+
**Android**: Uses SMS Retriever API to auto-read OTP from incoming SMS (no permissions required). SMS must include your app hash.
|
|
7382
|
+
|
|
7383
|
+
**iOS**: OTP is handled natively by the iOS keyboard. Use \`OTP_INPUT_PROPS\` on your TextInput to enable it.
|
|
7384
|
+
|
|
7385
|
+
**Web**: Returns no-op values.
|
|
7386
|
+
|
|
7387
|
+
\`\`\`tsx
|
|
7388
|
+
const {
|
|
7389
|
+
code, // string | null - received OTP code (Android only)
|
|
7390
|
+
startListening, // () => void - begin listening for SMS (Android only)
|
|
7391
|
+
stopListening, // () => void - stop listening (Android only)
|
|
7392
|
+
hash, // string | null - app hash for SMS body (Android only)
|
|
7393
|
+
} = useOTPAutoFill(options?: {
|
|
7394
|
+
codeLength?: number; // default: 6
|
|
7395
|
+
onCodeReceived?: (code: string) => void;
|
|
7396
|
+
});
|
|
7397
|
+
\`\`\`
|
|
7398
|
+
|
|
7399
|
+
### Options
|
|
7400
|
+
|
|
7401
|
+
| Option | Type | Default | Description |
|
|
7402
|
+
|--------|------|---------|-------------|
|
|
7403
|
+
| codeLength | number | 6 | Expected digit count of OTP code |
|
|
7404
|
+
| onCodeReceived | (code: string) => void | \u2014 | Callback when OTP is detected |
|
|
7405
|
+
|
|
7406
|
+
### Return Value
|
|
7407
|
+
|
|
7408
|
+
| Property | Type | Description |
|
|
7409
|
+
|----------|------|-------------|
|
|
7410
|
+
| code | string \\| null | Detected OTP code (Android). Null on iOS/web. |
|
|
7411
|
+
| startListening | () => void | Start SMS listener (Android). No-op on iOS/web. |
|
|
7412
|
+
| stopListening | () => void | Stop SMS listener (Android). No-op on iOS/web. |
|
|
7413
|
+
| hash | string \\| null | App hash for SMS Retriever (Android). Null on iOS/web. |
|
|
7414
|
+
|
|
7415
|
+
### Android SMS Format
|
|
7416
|
+
|
|
7417
|
+
For the SMS Retriever API to detect the message, the SMS must:
|
|
7418
|
+
1. Start with \`<#>\`
|
|
7419
|
+
2. Contain the OTP code as a sequence of digits
|
|
7420
|
+
3. End with your app's 11-character hash (available via \`hash\`)
|
|
7421
|
+
|
|
7422
|
+
Example SMS:
|
|
7423
|
+
\`\`\`
|
|
7424
|
+
<#> Your verification code is: 123456
|
|
7425
|
+
FA+9qCX9VSu
|
|
7426
|
+
\`\`\`
|
|
7427
|
+
|
|
7428
|
+
---
|
|
7429
|
+
|
|
7430
|
+
## OTP_INPUT_PROPS
|
|
7431
|
+
|
|
7432
|
+
Constant with TextInput props to enable native OTP keyboard autofill.
|
|
7433
|
+
|
|
7434
|
+
\`\`\`tsx
|
|
7435
|
+
import { OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
7436
|
+
|
|
7437
|
+
// Value:
|
|
7438
|
+
// {
|
|
7439
|
+
// textContentType: 'oneTimeCode',
|
|
7440
|
+
// autoComplete: 'sms-otp',
|
|
7441
|
+
// }
|
|
7442
|
+
|
|
7443
|
+
<TextInput
|
|
7444
|
+
{...OTP_INPUT_PROPS}
|
|
7445
|
+
value={code}
|
|
7446
|
+
onChangeText={setCode}
|
|
7447
|
+
/>
|
|
7448
|
+
\`\`\`
|
|
7449
|
+
|
|
7450
|
+
- **iOS**: Enables the keyboard to suggest OTP codes from received SMS (iOS 12+)
|
|
7451
|
+
- **Android**: Maps to the correct \`autoComplete\` value
|
|
7452
|
+
- **Web**: Harmless \u2014 ignored by web TextInput
|
|
7453
|
+
`,
|
|
7454
|
+
"idealyst://clipboard/examples": `# Clipboard Examples
|
|
7455
|
+
|
|
7456
|
+
Complete code examples for common @idealyst/clipboard patterns.
|
|
7457
|
+
|
|
7458
|
+
## Copy to Clipboard with Feedback
|
|
7459
|
+
|
|
7460
|
+
\`\`\`tsx
|
|
7461
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7462
|
+
import { useState, useCallback } from 'react';
|
|
7463
|
+
|
|
7464
|
+
function CopyButton({ text }: { text: string }) {
|
|
7465
|
+
const [copied, setCopied] = useState(false);
|
|
7466
|
+
|
|
7467
|
+
const handleCopy = useCallback(async () => {
|
|
7468
|
+
await clipboard.copy(text);
|
|
7469
|
+
setCopied(true);
|
|
7470
|
+
setTimeout(() => setCopied(false), 2000);
|
|
7471
|
+
}, [text]);
|
|
7472
|
+
|
|
7473
|
+
return (
|
|
7474
|
+
<Button
|
|
7475
|
+
label={copied ? 'Copied!' : 'Copy'}
|
|
7476
|
+
intent={copied ? 'positive' : 'neutral'}
|
|
7477
|
+
onPress={handleCopy}
|
|
7478
|
+
/>
|
|
7479
|
+
);
|
|
7480
|
+
}
|
|
7481
|
+
\`\`\`
|
|
7482
|
+
|
|
7483
|
+
## Share / Copy Link
|
|
7484
|
+
|
|
7485
|
+
\`\`\`tsx
|
|
7486
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7487
|
+
|
|
7488
|
+
async function copyShareLink(itemId: string) {
|
|
7489
|
+
const url = \`https://myapp.com/items/\${itemId}\`;
|
|
7490
|
+
await clipboard.copy(url);
|
|
7491
|
+
}
|
|
7492
|
+
\`\`\`
|
|
7493
|
+
|
|
7494
|
+
## Paste from Clipboard
|
|
7495
|
+
|
|
7496
|
+
\`\`\`tsx
|
|
7497
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7498
|
+
|
|
7499
|
+
function PasteInput() {
|
|
7500
|
+
const [value, setValue] = useState('');
|
|
7501
|
+
|
|
7502
|
+
const handlePaste = useCallback(async () => {
|
|
7503
|
+
const hasText = await clipboard.hasText();
|
|
7504
|
+
if (hasText) {
|
|
7505
|
+
const text = await clipboard.paste();
|
|
7506
|
+
setValue(text);
|
|
7507
|
+
}
|
|
7508
|
+
}, []);
|
|
7509
|
+
|
|
7510
|
+
return (
|
|
7511
|
+
<View>
|
|
7512
|
+
<Input value={value} onChangeText={setValue} placeholder="Enter or paste text" />
|
|
7513
|
+
<Button label="Paste" onPress={handlePaste} iconName="content-paste" />
|
|
7514
|
+
</View>
|
|
7515
|
+
);
|
|
7516
|
+
}
|
|
7517
|
+
\`\`\`
|
|
7518
|
+
|
|
7519
|
+
## useClipboard Hook
|
|
7520
|
+
|
|
7521
|
+
\`\`\`tsx
|
|
7522
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7523
|
+
import { useState, useCallback } from 'react';
|
|
7524
|
+
|
|
7525
|
+
export function useClipboard() {
|
|
7526
|
+
const [copiedText, setCopiedText] = useState<string | null>(null);
|
|
7527
|
+
|
|
7528
|
+
const copy = useCallback(async (text: string) => {
|
|
7529
|
+
await clipboard.copy(text);
|
|
7530
|
+
setCopiedText(text);
|
|
7531
|
+
}, []);
|
|
7532
|
+
|
|
7533
|
+
const paste = useCallback(async () => {
|
|
7534
|
+
return clipboard.paste();
|
|
7535
|
+
}, []);
|
|
7536
|
+
|
|
7537
|
+
const reset = useCallback(() => {
|
|
7538
|
+
setCopiedText(null);
|
|
7539
|
+
}, []);
|
|
7540
|
+
|
|
7541
|
+
return { copy, paste, copiedText, reset };
|
|
7542
|
+
}
|
|
7543
|
+
\`\`\`
|
|
7544
|
+
|
|
7545
|
+
## OTP Verification Screen
|
|
7546
|
+
|
|
7547
|
+
\`\`\`tsx
|
|
7548
|
+
import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
7549
|
+
import { useEffect, useState } from 'react';
|
|
7550
|
+
import { TextInput, Platform } from 'react-native';
|
|
7551
|
+
|
|
7552
|
+
function OTPVerificationScreen({ phoneNumber, onVerify }: {
|
|
7553
|
+
phoneNumber: string;
|
|
7554
|
+
onVerify: (code: string) => void;
|
|
7555
|
+
}) {
|
|
7556
|
+
const [code, setCode] = useState('');
|
|
7557
|
+
|
|
7558
|
+
const otp = useOTPAutoFill({
|
|
7559
|
+
codeLength: 6,
|
|
7560
|
+
onCodeReceived: (receivedCode) => {
|
|
7561
|
+
setCode(receivedCode);
|
|
7562
|
+
onVerify(receivedCode);
|
|
7563
|
+
},
|
|
7564
|
+
});
|
|
7565
|
+
|
|
7566
|
+
// Start listening when screen mounts
|
|
7567
|
+
useEffect(() => {
|
|
7568
|
+
otp.startListening();
|
|
7569
|
+
return () => otp.stopListening();
|
|
7570
|
+
}, []);
|
|
7571
|
+
|
|
7572
|
+
// Auto-fill from hook on Android
|
|
7573
|
+
useEffect(() => {
|
|
7574
|
+
if (otp.code) {
|
|
7575
|
+
setCode(otp.code);
|
|
7576
|
+
}
|
|
7577
|
+
}, [otp.code]);
|
|
7578
|
+
|
|
7579
|
+
return (
|
|
7580
|
+
<View>
|
|
7581
|
+
<Text>Enter the code sent to {phoneNumber}</Text>
|
|
7582
|
+
|
|
7583
|
+
<TextInput
|
|
7584
|
+
value={code}
|
|
7585
|
+
onChangeText={(text) => {
|
|
7586
|
+
setCode(text);
|
|
7587
|
+
if (text.length === 6) onVerify(text);
|
|
7588
|
+
}}
|
|
7589
|
+
keyboardType="number-pad"
|
|
7590
|
+
maxLength={6}
|
|
7591
|
+
{...OTP_INPUT_PROPS}
|
|
7592
|
+
/>
|
|
7593
|
+
|
|
7594
|
+
{Platform.OS === 'android' && otp.hash && (
|
|
7595
|
+
<Text style={{ fontSize: 10, color: '#999' }}>
|
|
7596
|
+
App hash (for SMS setup): {otp.hash}
|
|
7597
|
+
</Text>
|
|
7598
|
+
)}
|
|
7599
|
+
</View>
|
|
7600
|
+
);
|
|
7601
|
+
}
|
|
7602
|
+
\`\`\`
|
|
7603
|
+
|
|
7604
|
+
## Copy Invite Code
|
|
7605
|
+
|
|
7606
|
+
\`\`\`tsx
|
|
7607
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7608
|
+
|
|
7609
|
+
function InviteCard({ code }: { code: string }) {
|
|
7610
|
+
const [copied, setCopied] = useState(false);
|
|
7611
|
+
|
|
7612
|
+
return (
|
|
7613
|
+
<Card>
|
|
7614
|
+
<Text>Your invite code</Text>
|
|
7615
|
+
<Text variant="headline">{code}</Text>
|
|
7616
|
+
<Button
|
|
7617
|
+
label={copied ? 'Copied!' : 'Copy Code'}
|
|
7618
|
+
iconName={copied ? 'check' : 'content-copy'}
|
|
7619
|
+
onPress={async () => {
|
|
7620
|
+
await clipboard.copy(code);
|
|
7621
|
+
setCopied(true);
|
|
7622
|
+
setTimeout(() => setCopied(false), 2000);
|
|
7623
|
+
}}
|
|
7624
|
+
/>
|
|
7625
|
+
</Card>
|
|
7626
|
+
);
|
|
7627
|
+
}
|
|
7628
|
+
\`\`\`
|
|
7629
|
+
|
|
7630
|
+
## Error Handling
|
|
7631
|
+
|
|
7632
|
+
\`\`\`tsx
|
|
7633
|
+
import { clipboard } from '@idealyst/clipboard';
|
|
7634
|
+
|
|
7635
|
+
async function safeCopy(text: string): Promise<boolean> {
|
|
7636
|
+
try {
|
|
7637
|
+
await clipboard.copy(text);
|
|
7638
|
+
return true;
|
|
7639
|
+
} catch (error) {
|
|
7640
|
+
console.error('Clipboard copy failed:', error);
|
|
7641
|
+
// On web, this can fail if not in a secure context or without user gesture
|
|
7642
|
+
return false;
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
|
|
7646
|
+
async function safePaste(): Promise<string | null> {
|
|
7647
|
+
try {
|
|
7648
|
+
const hasText = await clipboard.hasText();
|
|
7649
|
+
if (!hasText) return null;
|
|
7650
|
+
return await clipboard.paste();
|
|
7651
|
+
} catch (error) {
|
|
7652
|
+
console.error('Clipboard paste failed:', error);
|
|
7653
|
+
// Browser may deny permission
|
|
7654
|
+
return null;
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7657
|
+
\`\`\`
|
|
7658
|
+
|
|
7659
|
+
## Best Practices
|
|
7660
|
+
|
|
7661
|
+
1. **Always use try-catch** \u2014 Clipboard operations can fail (permissions, secure context)
|
|
7662
|
+
2. **Provide visual feedback** \u2014 Show a "Copied!" confirmation after copying
|
|
7663
|
+
3. **Use OTP_INPUT_PROPS** \u2014 Always spread these on OTP text inputs for cross-platform autofill
|
|
7664
|
+
4. **Start OTP listener on mount** \u2014 Call \`startListening()\` in useEffect, clean up with \`stopListening()\`
|
|
7665
|
+
5. **Log the Android hash** \u2014 During development, log \`hash\` to configure your SMS gateway
|
|
7666
|
+
6. **Graceful degradation** \u2014 OTP features degrade gracefully if native modules aren't installed
|
|
7667
|
+
7. **Secure context (web)** \u2014 Clipboard API requires HTTPS on web
|
|
7668
|
+
`
|
|
7669
|
+
};
|
|
7670
|
+
|
|
7671
|
+
// src/data/biometrics-guides.ts
|
|
7672
|
+
var biometricsGuides = {
|
|
7673
|
+
"idealyst://biometrics/overview": `# @idealyst/biometrics Overview
|
|
7674
|
+
|
|
7675
|
+
Cross-platform biometric authentication and passkeys (WebAuthn/FIDO2) for React and React Native applications.
|
|
7676
|
+
|
|
7677
|
+
## Features
|
|
7678
|
+
|
|
7679
|
+
- **Local Biometric Auth** \u2014 FaceID, TouchID, fingerprint, iris to gate access
|
|
7680
|
+
- **Passkeys (WebAuthn/FIDO2)** \u2014 Passwordless login with cryptographic credentials
|
|
7681
|
+
- **Cross-Platform** \u2014 Single API for React Native (iOS/Android) and Web
|
|
7682
|
+
- **React Native** \u2014 Uses expo-local-authentication for biometrics, react-native-passkeys for passkeys
|
|
7683
|
+
- **Web** \u2014 Uses WebAuthn API for both biometrics and passkeys
|
|
7684
|
+
- **Graceful Degradation** \u2014 Falls back cleanly when native modules aren't installed
|
|
7685
|
+
- **TypeScript** \u2014 Full type safety and IntelliSense support
|
|
7686
|
+
|
|
7687
|
+
## Installation
|
|
7688
|
+
|
|
7689
|
+
\`\`\`bash
|
|
7690
|
+
yarn add @idealyst/biometrics
|
|
7691
|
+
|
|
7692
|
+
# React Native \u2014 biometric auth:
|
|
7693
|
+
yarn add expo-local-authentication
|
|
7694
|
+
cd ios && pod install
|
|
7695
|
+
|
|
7696
|
+
# React Native \u2014 passkeys (optional):
|
|
7697
|
+
yarn add react-native-passkeys
|
|
7698
|
+
cd ios && pod install
|
|
7699
|
+
\`\`\`
|
|
7700
|
+
|
|
7701
|
+
## Quick Start \u2014 Biometric Auth
|
|
7702
|
+
|
|
7703
|
+
\`\`\`tsx
|
|
7704
|
+
import { isBiometricAvailable, authenticate } from '@idealyst/biometrics';
|
|
7705
|
+
|
|
7706
|
+
// Check availability
|
|
7707
|
+
const available = await isBiometricAvailable();
|
|
7708
|
+
|
|
7709
|
+
// Prompt user
|
|
7710
|
+
if (available) {
|
|
7711
|
+
const result = await authenticate({
|
|
7712
|
+
promptMessage: 'Verify your identity',
|
|
7713
|
+
});
|
|
7714
|
+
|
|
7715
|
+
if (result.success) {
|
|
7716
|
+
// Authenticated!
|
|
7717
|
+
} else {
|
|
7718
|
+
console.log(result.error, result.message);
|
|
7719
|
+
}
|
|
7720
|
+
}
|
|
7721
|
+
\`\`\`
|
|
7722
|
+
|
|
7723
|
+
## Quick Start \u2014 Passkeys
|
|
7724
|
+
|
|
7725
|
+
\`\`\`tsx
|
|
7726
|
+
import { isPasskeySupported, createPasskey, getPasskey } from '@idealyst/biometrics';
|
|
7727
|
+
|
|
7728
|
+
// Check support
|
|
7729
|
+
const supported = await isPasskeySupported();
|
|
7730
|
+
|
|
7731
|
+
// Register a new passkey
|
|
7732
|
+
const credential = await createPasskey({
|
|
7733
|
+
challenge: serverChallenge,
|
|
7734
|
+
rp: { id: 'example.com', name: 'My App' },
|
|
7735
|
+
user: { id: userId, name: email, displayName: name },
|
|
7736
|
+
});
|
|
7737
|
+
// Send credential to server for verification
|
|
7738
|
+
|
|
7739
|
+
// Sign in with passkey
|
|
7740
|
+
const assertion = await getPasskey({
|
|
7741
|
+
challenge: serverChallenge,
|
|
7742
|
+
rpId: 'example.com',
|
|
7743
|
+
});
|
|
7744
|
+
// Send assertion to server for verification
|
|
7745
|
+
\`\`\`
|
|
7746
|
+
|
|
7747
|
+
## Platform Details
|
|
7748
|
+
|
|
7749
|
+
- **React Native (biometrics)**: Uses \`expo-local-authentication\` \u2014 FaceID, TouchID, fingerprint, iris
|
|
7750
|
+
- **React Native (passkeys)**: Uses \`react-native-passkeys\` \u2014 system passkey UI on iOS 16+ and Android 9+
|
|
7751
|
+
- **Web (biometrics)**: Uses WebAuthn with \`userVerification: 'required'\` to trigger platform authenticator
|
|
7752
|
+
- **Web (passkeys)**: Uses \`navigator.credentials.create/get\` with the PublicKey API
|
|
7753
|
+
`,
|
|
7754
|
+
"idealyst://biometrics/api": `# Biometrics API Reference
|
|
7755
|
+
|
|
7756
|
+
Complete API reference for @idealyst/biometrics.
|
|
7757
|
+
|
|
7758
|
+
---
|
|
7759
|
+
|
|
7760
|
+
## Biometric Authentication Functions
|
|
7761
|
+
|
|
7762
|
+
### isBiometricAvailable
|
|
7763
|
+
|
|
7764
|
+
Check whether biometric auth hardware is available and enrolled.
|
|
7765
|
+
|
|
7766
|
+
\`\`\`tsx
|
|
7767
|
+
await isBiometricAvailable(): Promise<boolean>
|
|
7768
|
+
|
|
7769
|
+
// Native: checks hardware + enrollment via expo-local-authentication
|
|
7770
|
+
// Web: checks PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
|
|
7771
|
+
\`\`\`
|
|
7772
|
+
|
|
7773
|
+
### getBiometricTypes
|
|
7774
|
+
|
|
7775
|
+
Return the biometric types available on this device.
|
|
7776
|
+
|
|
7777
|
+
\`\`\`tsx
|
|
7778
|
+
await getBiometricTypes(): Promise<BiometricType[]>
|
|
7779
|
+
|
|
7780
|
+
type BiometricType = 'fingerprint' | 'facial_recognition' | 'iris';
|
|
7781
|
+
|
|
7782
|
+
// Native: returns specific types (fingerprint, facial_recognition, iris)
|
|
7783
|
+
// Web: returns ['fingerprint'] as generic indicator, or [] if unavailable
|
|
7784
|
+
\`\`\`
|
|
7785
|
+
|
|
7786
|
+
### getSecurityLevel
|
|
7787
|
+
|
|
7788
|
+
Get the security level of biometric authentication on the device.
|
|
7789
|
+
|
|
7790
|
+
\`\`\`tsx
|
|
7791
|
+
await getSecurityLevel(): Promise<SecurityLevel>
|
|
7792
|
+
|
|
7793
|
+
type SecurityLevel = 'none' | 'device_credential' | 'biometric_weak' | 'biometric_strong';
|
|
7794
|
+
|
|
7795
|
+
// Native: maps expo-local-authentication SecurityLevel enum
|
|
7796
|
+
// Web: returns 'biometric_strong' if platform authenticator available, else 'none'
|
|
7797
|
+
\`\`\`
|
|
7798
|
+
|
|
7799
|
+
### authenticate
|
|
7800
|
+
|
|
7801
|
+
Prompt the user for biometric authentication.
|
|
7802
|
+
|
|
7803
|
+
\`\`\`tsx
|
|
7804
|
+
await authenticate(options?: AuthenticateOptions): Promise<AuthResult>
|
|
7805
|
+
\`\`\`
|
|
7806
|
+
|
|
7807
|
+
**AuthenticateOptions:**
|
|
7808
|
+
|
|
7809
|
+
| Option | Type | Default | Description |
|
|
7810
|
+
|--------|------|---------|-------------|
|
|
7811
|
+
| promptMessage | string | 'Authenticate' | Message shown alongside the biometric prompt |
|
|
7812
|
+
| cancelLabel | string | \u2014 | Label for the cancel button |
|
|
7813
|
+
| fallbackLabel | string | \u2014 | iOS: label for passcode fallback button |
|
|
7814
|
+
| disableDeviceFallback | boolean | false | Prevent PIN/passcode fallback after biometric failure |
|
|
7815
|
+
| requireStrongBiometric | boolean | false | Android: require Class 3 (strong) biometric |
|
|
7816
|
+
|
|
7817
|
+
**AuthResult:**
|
|
7818
|
+
|
|
7819
|
+
\`\`\`tsx
|
|
7820
|
+
type AuthResult =
|
|
7821
|
+
| { success: true }
|
|
7822
|
+
| { success: false; error: AuthError; message?: string };
|
|
7823
|
+
|
|
7824
|
+
type AuthError =
|
|
7825
|
+
| 'not_available'
|
|
7826
|
+
| 'not_enrolled'
|
|
7827
|
+
| 'user_cancel'
|
|
7828
|
+
| 'lockout'
|
|
7829
|
+
| 'system_cancel'
|
|
7830
|
+
| 'passcode_not_set'
|
|
7831
|
+
| 'authentication_failed'
|
|
7832
|
+
| 'unknown';
|
|
7833
|
+
\`\`\`
|
|
7834
|
+
|
|
7835
|
+
### cancelAuthentication
|
|
7836
|
+
|
|
7837
|
+
Cancel an in-progress authentication prompt (Android only). No-op on iOS and web.
|
|
7838
|
+
|
|
7839
|
+
\`\`\`tsx
|
|
7840
|
+
await cancelAuthentication(): Promise<void>
|
|
7841
|
+
\`\`\`
|
|
7842
|
+
|
|
7843
|
+
---
|
|
7844
|
+
|
|
7845
|
+
## Passkey Functions
|
|
7846
|
+
|
|
7847
|
+
### isPasskeySupported
|
|
7848
|
+
|
|
7849
|
+
Check if passkeys (WebAuthn/FIDO2) are supported on this device/browser.
|
|
7850
|
+
|
|
7851
|
+
\`\`\`tsx
|
|
7852
|
+
await isPasskeySupported(): Promise<boolean>
|
|
7853
|
+
|
|
7854
|
+
// Web: checks PublicKeyCredential + isUserVerifyingPlatformAuthenticatorAvailable
|
|
7855
|
+
// Native: checks react-native-passkeys Passkey.isSupported()
|
|
7856
|
+
\`\`\`
|
|
7857
|
+
|
|
7858
|
+
### createPasskey
|
|
7859
|
+
|
|
7860
|
+
Create a new passkey credential (registration / attestation ceremony).
|
|
7861
|
+
|
|
7862
|
+
\`\`\`tsx
|
|
7863
|
+
await createPasskey(options: PasskeyCreateOptions): Promise<PasskeyCreateResult>
|
|
7864
|
+
\`\`\`
|
|
7865
|
+
|
|
7866
|
+
**PasskeyCreateOptions:**
|
|
7867
|
+
|
|
7868
|
+
| Option | Type | Required | Description |
|
|
7869
|
+
|--------|------|----------|-------------|
|
|
7870
|
+
| challenge | string | Yes | Base64url-encoded challenge from server |
|
|
7871
|
+
| rp | { id: string; name: string } | Yes | Relying party info |
|
|
7872
|
+
| user | { id: string; name: string; displayName: string } | Yes | User info (id is base64url) |
|
|
7873
|
+
| pubKeyCredParams | PublicKeyCredentialParam[] | No | Defaults to ES256 + RS256 |
|
|
7874
|
+
| timeout | number | No | Timeout in ms (default 60000) |
|
|
7875
|
+
| authenticatorSelection | object | No | Authenticator criteria |
|
|
7876
|
+
| excludeCredentials | CredentialDescriptor[] | No | Prevent re-registration |
|
|
7877
|
+
| attestation | string | No | 'none' \\| 'indirect' \\| 'direct' \\| 'enterprise' |
|
|
7878
|
+
|
|
7879
|
+
**PasskeyCreateResult:**
|
|
7880
|
+
|
|
7881
|
+
| Property | Type | Description |
|
|
7882
|
+
|----------|------|-------------|
|
|
7883
|
+
| id | string | Credential ID (base64url) |
|
|
7884
|
+
| rawId | string | Raw credential ID (base64url) |
|
|
7885
|
+
| type | 'public-key' | Always 'public-key' |
|
|
7886
|
+
| response.clientDataJSON | string | Client data (base64url) |
|
|
7887
|
+
| response.attestationObject | string | Attestation object (base64url) |
|
|
7888
|
+
|
|
7889
|
+
### getPasskey
|
|
7890
|
+
|
|
7891
|
+
Authenticate with an existing passkey (assertion ceremony).
|
|
7892
|
+
|
|
7893
|
+
\`\`\`tsx
|
|
7894
|
+
await getPasskey(options: PasskeyGetOptions): Promise<PasskeyGetResult>
|
|
7895
|
+
\`\`\`
|
|
7896
|
+
|
|
7897
|
+
**PasskeyGetOptions:**
|
|
7898
|
+
|
|
7899
|
+
| Option | Type | Required | Description |
|
|
7900
|
+
|--------|------|----------|-------------|
|
|
7901
|
+
| challenge | string | Yes | Base64url-encoded challenge from server |
|
|
7902
|
+
| rpId | string | No | Relying party ID |
|
|
7903
|
+
| allowCredentials | CredentialDescriptor[] | No | Allowed credentials (empty = discoverable) |
|
|
7904
|
+
| timeout | number | No | Timeout in ms (default 60000) |
|
|
7905
|
+
| userVerification | string | No | 'required' \\| 'preferred' \\| 'discouraged' |
|
|
7906
|
+
|
|
7907
|
+
**PasskeyGetResult:**
|
|
7908
|
+
|
|
7909
|
+
| Property | Type | Description |
|
|
7910
|
+
|----------|------|-------------|
|
|
7911
|
+
| id | string | Credential ID (base64url) |
|
|
7912
|
+
| rawId | string | Raw credential ID (base64url) |
|
|
7913
|
+
| type | 'public-key' | Always 'public-key' |
|
|
7914
|
+
| response.clientDataJSON | string | Client data (base64url) |
|
|
7915
|
+
| response.authenticatorData | string | Authenticator data (base64url) |
|
|
7916
|
+
| response.signature | string | Signature (base64url) |
|
|
7917
|
+
| response.userHandle | string \\| undefined | User handle (base64url) |
|
|
7918
|
+
|
|
7919
|
+
### PasskeyError
|
|
7920
|
+
|
|
7921
|
+
Both \`createPasskey\` and \`getPasskey\` throw a \`PasskeyError\` on failure:
|
|
7922
|
+
|
|
7923
|
+
\`\`\`tsx
|
|
7924
|
+
interface PasskeyError {
|
|
7925
|
+
code: 'not_supported' | 'cancelled' | 'invalid_state' | 'not_allowed' | 'unknown';
|
|
7926
|
+
message: string;
|
|
7927
|
+
}
|
|
7928
|
+
|
|
7929
|
+
try {
|
|
7930
|
+
const result = await createPasskey(options);
|
|
7931
|
+
} catch (err) {
|
|
7932
|
+
const passkeyErr = err as PasskeyError;
|
|
7933
|
+
console.log(passkeyErr.code, passkeyErr.message);
|
|
7934
|
+
}
|
|
7935
|
+
\`\`\`
|
|
7936
|
+
|
|
7937
|
+
---
|
|
7938
|
+
|
|
7939
|
+
## Base64url Helpers
|
|
7940
|
+
|
|
7941
|
+
Shared utilities for encoding/decoding WebAuthn binary data.
|
|
7942
|
+
|
|
7943
|
+
\`\`\`tsx
|
|
7944
|
+
import { base64urlToBuffer, bufferToBase64url } from '@idealyst/biometrics';
|
|
7945
|
+
|
|
7946
|
+
// Convert base64url string to ArrayBuffer
|
|
7947
|
+
const buffer: ArrayBuffer = base64urlToBuffer(base64urlString);
|
|
7948
|
+
|
|
7949
|
+
// Convert ArrayBuffer to base64url string
|
|
7950
|
+
const str: string = bufferToBase64url(arrayBuffer);
|
|
7951
|
+
\`\`\`
|
|
7952
|
+
`,
|
|
7953
|
+
"idealyst://biometrics/examples": `# Biometrics Examples
|
|
7954
|
+
|
|
7955
|
+
Complete code examples for common @idealyst/biometrics patterns.
|
|
7956
|
+
|
|
7957
|
+
## Gate Screen Access with Biometrics
|
|
7958
|
+
|
|
7959
|
+
\`\`\`tsx
|
|
7960
|
+
import { isBiometricAvailable, authenticate } from '@idealyst/biometrics';
|
|
7961
|
+
import { useEffect, useState } from 'react';
|
|
7962
|
+
|
|
7963
|
+
function ProtectedScreen({ children }: { children: React.ReactNode }) {
|
|
7964
|
+
const [unlocked, setUnlocked] = useState(false);
|
|
7965
|
+
const [error, setError] = useState<string | null>(null);
|
|
7966
|
+
|
|
7967
|
+
const unlock = async () => {
|
|
7968
|
+
const available = await isBiometricAvailable();
|
|
7969
|
+
if (!available) {
|
|
7970
|
+
// No biometrics \u2014 fall back to PIN or allow access
|
|
7971
|
+
setUnlocked(true);
|
|
7972
|
+
return;
|
|
7973
|
+
}
|
|
7974
|
+
|
|
7975
|
+
const result = await authenticate({
|
|
7976
|
+
promptMessage: 'Unlock to continue',
|
|
7977
|
+
cancelLabel: 'Cancel',
|
|
7978
|
+
});
|
|
7979
|
+
|
|
7980
|
+
if (result.success) {
|
|
7981
|
+
setUnlocked(true);
|
|
7982
|
+
} else {
|
|
7983
|
+
setError(result.message ?? 'Authentication failed');
|
|
7984
|
+
}
|
|
7985
|
+
};
|
|
7986
|
+
|
|
7987
|
+
useEffect(() => {
|
|
7988
|
+
unlock();
|
|
7989
|
+
}, []);
|
|
7990
|
+
|
|
7991
|
+
if (!unlocked) {
|
|
7992
|
+
return (
|
|
7993
|
+
<View>
|
|
7994
|
+
<Text>Please authenticate to continue</Text>
|
|
7995
|
+
{error && <Text intent="negative">{error}</Text>}
|
|
7996
|
+
<Button label="Try Again" onPress={unlock} />
|
|
7997
|
+
</View>
|
|
7998
|
+
);
|
|
7999
|
+
}
|
|
8000
|
+
|
|
8001
|
+
return <>{children}</>;
|
|
8002
|
+
}
|
|
8003
|
+
\`\`\`
|
|
8004
|
+
|
|
8005
|
+
## Confirm Sensitive Action
|
|
8006
|
+
|
|
8007
|
+
\`\`\`tsx
|
|
8008
|
+
import { authenticate } from '@idealyst/biometrics';
|
|
8009
|
+
|
|
8010
|
+
async function confirmTransfer(amount: number, recipient: string) {
|
|
8011
|
+
const result = await authenticate({
|
|
8012
|
+
promptMessage: \`Confirm transfer of $\${amount} to \${recipient}\`,
|
|
8013
|
+
disableDeviceFallback: false,
|
|
8014
|
+
});
|
|
8015
|
+
|
|
8016
|
+
if (!result.success) {
|
|
8017
|
+
throw new Error(result.message ?? 'Authentication required');
|
|
8018
|
+
}
|
|
8019
|
+
|
|
8020
|
+
// Proceed with transfer
|
|
8021
|
+
await api.transfer({ amount, recipient });
|
|
8022
|
+
}
|
|
8023
|
+
\`\`\`
|
|
8024
|
+
|
|
8025
|
+
## Show Biometric Info
|
|
8026
|
+
|
|
8027
|
+
\`\`\`tsx
|
|
8028
|
+
import {
|
|
8029
|
+
isBiometricAvailable,
|
|
8030
|
+
getBiometricTypes,
|
|
8031
|
+
getSecurityLevel,
|
|
8032
|
+
} from '@idealyst/biometrics';
|
|
8033
|
+
|
|
8034
|
+
function BiometricSettings() {
|
|
8035
|
+
const [info, setInfo] = useState({
|
|
8036
|
+
available: false,
|
|
8037
|
+
types: [] as string[],
|
|
8038
|
+
level: 'none',
|
|
8039
|
+
});
|
|
8040
|
+
|
|
8041
|
+
useEffect(() => {
|
|
8042
|
+
async function load() {
|
|
8043
|
+
const [available, types, level] = await Promise.all([
|
|
8044
|
+
isBiometricAvailable(),
|
|
8045
|
+
getBiometricTypes(),
|
|
8046
|
+
getSecurityLevel(),
|
|
8047
|
+
]);
|
|
8048
|
+
setInfo({ available, types, level });
|
|
8049
|
+
}
|
|
8050
|
+
load();
|
|
8051
|
+
}, []);
|
|
8052
|
+
|
|
8053
|
+
return (
|
|
8054
|
+
<Card>
|
|
8055
|
+
<Text>Biometric available: {info.available ? 'Yes' : 'No'}</Text>
|
|
8056
|
+
<Text>Types: {info.types.join(', ') || 'None'}</Text>
|
|
8057
|
+
<Text>Security level: {info.level}</Text>
|
|
8058
|
+
</Card>
|
|
8059
|
+
);
|
|
8060
|
+
}
|
|
8061
|
+
\`\`\`
|
|
8062
|
+
|
|
8063
|
+
## Passkey Registration Flow
|
|
8064
|
+
|
|
8065
|
+
\`\`\`tsx
|
|
8066
|
+
import { isPasskeySupported, createPasskey } from '@idealyst/biometrics';
|
|
8067
|
+
import type { PasskeyError } from '@idealyst/biometrics';
|
|
8068
|
+
|
|
8069
|
+
async function registerPasskey(user: { id: string; email: string; name: string }) {
|
|
8070
|
+
const supported = await isPasskeySupported();
|
|
8071
|
+
if (!supported) {
|
|
8072
|
+
alert('Passkeys are not supported on this device');
|
|
8073
|
+
return;
|
|
8074
|
+
}
|
|
8075
|
+
|
|
8076
|
+
// 1. Get challenge from server
|
|
8077
|
+
const { challenge, rpId, rpName } = await api.getRegistrationChallenge();
|
|
8078
|
+
|
|
8079
|
+
try {
|
|
8080
|
+
// 2. Create the passkey
|
|
8081
|
+
const credential = await createPasskey({
|
|
8082
|
+
challenge,
|
|
8083
|
+
rp: { id: rpId, name: rpName },
|
|
8084
|
+
user: {
|
|
8085
|
+
id: user.id,
|
|
8086
|
+
name: user.email,
|
|
8087
|
+
displayName: user.name,
|
|
8088
|
+
},
|
|
8089
|
+
authenticatorSelection: {
|
|
8090
|
+
residentKey: 'required',
|
|
8091
|
+
userVerification: 'required',
|
|
8092
|
+
},
|
|
8093
|
+
});
|
|
8094
|
+
|
|
8095
|
+
// 3. Send to server for verification and storage
|
|
8096
|
+
await api.verifyRegistration({
|
|
8097
|
+
id: credential.id,
|
|
8098
|
+
rawId: credential.rawId,
|
|
8099
|
+
clientDataJSON: credential.response.clientDataJSON,
|
|
8100
|
+
attestationObject: credential.response.attestationObject,
|
|
8101
|
+
});
|
|
8102
|
+
|
|
8103
|
+
alert('Passkey registered successfully!');
|
|
8104
|
+
} catch (err) {
|
|
8105
|
+
const passkeyErr = err as PasskeyError;
|
|
8106
|
+
if (passkeyErr.code === 'cancelled') {
|
|
8107
|
+
// User cancelled \u2014 do nothing
|
|
8108
|
+
return;
|
|
8109
|
+
}
|
|
8110
|
+
alert(\`Failed to register passkey: \${passkeyErr.message}\`);
|
|
8111
|
+
}
|
|
8112
|
+
}
|
|
8113
|
+
\`\`\`
|
|
8114
|
+
|
|
8115
|
+
## Passkey Login Flow
|
|
8116
|
+
|
|
8117
|
+
\`\`\`tsx
|
|
8118
|
+
import { getPasskey } from '@idealyst/biometrics';
|
|
8119
|
+
import type { PasskeyError } from '@idealyst/biometrics';
|
|
8120
|
+
|
|
8121
|
+
async function loginWithPasskey() {
|
|
8122
|
+
// 1. Get challenge from server
|
|
8123
|
+
const { challenge, rpId } = await api.getAuthenticationChallenge();
|
|
8124
|
+
|
|
8125
|
+
try {
|
|
8126
|
+
// 2. Authenticate with passkey
|
|
8127
|
+
const assertion = await getPasskey({
|
|
8128
|
+
challenge,
|
|
8129
|
+
rpId,
|
|
8130
|
+
userVerification: 'required',
|
|
8131
|
+
});
|
|
8132
|
+
|
|
8133
|
+
// 3. Send to server for verification
|
|
8134
|
+
const session = await api.verifyAuthentication({
|
|
8135
|
+
id: assertion.id,
|
|
8136
|
+
rawId: assertion.rawId,
|
|
8137
|
+
clientDataJSON: assertion.response.clientDataJSON,
|
|
8138
|
+
authenticatorData: assertion.response.authenticatorData,
|
|
8139
|
+
signature: assertion.response.signature,
|
|
8140
|
+
userHandle: assertion.response.userHandle,
|
|
8141
|
+
});
|
|
8142
|
+
|
|
8143
|
+
return session;
|
|
8144
|
+
} catch (err) {
|
|
8145
|
+
const passkeyErr = err as PasskeyError;
|
|
8146
|
+
if (passkeyErr.code === 'cancelled') return null;
|
|
8147
|
+
throw passkeyErr;
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
8150
|
+
\`\`\`
|
|
8151
|
+
|
|
8152
|
+
## Login Screen with Passkey + Fallback
|
|
8153
|
+
|
|
8154
|
+
\`\`\`tsx
|
|
8155
|
+
import { isPasskeySupported, getPasskey } from '@idealyst/biometrics';
|
|
8156
|
+
import type { PasskeyError } from '@idealyst/biometrics';
|
|
8157
|
+
|
|
8158
|
+
function LoginScreen() {
|
|
8159
|
+
const [passkeyAvailable, setPasskeyAvailable] = useState(false);
|
|
8160
|
+
const [loading, setLoading] = useState(false);
|
|
8161
|
+
|
|
8162
|
+
useEffect(() => {
|
|
8163
|
+
isPasskeySupported().then(setPasskeyAvailable);
|
|
8164
|
+
}, []);
|
|
8165
|
+
|
|
8166
|
+
const handlePasskeyLogin = async () => {
|
|
8167
|
+
setLoading(true);
|
|
8168
|
+
try {
|
|
8169
|
+
const { challenge, rpId } = await api.getAuthenticationChallenge();
|
|
8170
|
+
const assertion = await getPasskey({ challenge, rpId });
|
|
8171
|
+
const session = await api.verifyAuthentication(assertion);
|
|
8172
|
+
navigateToHome(session);
|
|
8173
|
+
} catch (err) {
|
|
8174
|
+
const e = err as PasskeyError;
|
|
8175
|
+
if (e.code !== 'cancelled') {
|
|
8176
|
+
showError(e.message);
|
|
8177
|
+
}
|
|
8178
|
+
} finally {
|
|
8179
|
+
setLoading(false);
|
|
8180
|
+
}
|
|
8181
|
+
};
|
|
8182
|
+
|
|
8183
|
+
return (
|
|
8184
|
+
<View>
|
|
8185
|
+
<Text variant="headline">Welcome Back</Text>
|
|
8186
|
+
|
|
8187
|
+
{passkeyAvailable && (
|
|
8188
|
+
<Button
|
|
8189
|
+
label="Sign in with Passkey"
|
|
8190
|
+
iconName="fingerprint"
|
|
8191
|
+
onPress={handlePasskeyLogin}
|
|
8192
|
+
loading={loading}
|
|
8193
|
+
/>
|
|
8194
|
+
)}
|
|
8195
|
+
|
|
8196
|
+
<Button
|
|
8197
|
+
label="Sign in with Email"
|
|
8198
|
+
intent="neutral"
|
|
8199
|
+
onPress={navigateToEmailLogin}
|
|
8200
|
+
/>
|
|
8201
|
+
</View>
|
|
8202
|
+
);
|
|
8203
|
+
}
|
|
8204
|
+
\`\`\`
|
|
8205
|
+
|
|
8206
|
+
## Best Practices
|
|
8207
|
+
|
|
8208
|
+
1. **Always check availability first** \u2014 Call \`isBiometricAvailable()\` or \`isPasskeySupported()\` before prompting
|
|
8209
|
+
2. **Provide fallbacks** \u2014 Not all devices support biometrics. Offer PIN/password alternatives
|
|
8210
|
+
3. **Handle cancellation gracefully** \u2014 Users may cancel the prompt. Don't show errors for \`user_cancel\` / \`cancelled\`
|
|
8211
|
+
4. **Use try/catch for passkeys** \u2014 Passkey functions throw \`PasskeyError\` on failure
|
|
8212
|
+
5. **Server-side validation** \u2014 Always verify passkey responses on your server. The client is untrusted
|
|
8213
|
+
6. **Base64url encoding** \u2014 All binary WebAuthn data is encoded as base64url strings for transport
|
|
8214
|
+
7. **Set rpId correctly** \u2014 The relying party ID must match your domain. On native, it's your associated domain
|
|
8215
|
+
8. **Lockout handling** \u2014 After too many failed biometric attempts, the device locks out. Handle the \`lockout\` error
|
|
8216
|
+
9. **iOS permissions** \u2014 FaceID requires \`NSFaceIDUsageDescription\` in Info.plist
|
|
8217
|
+
10. **Android associated domains** \u2014 Passkeys on Android require a Digital Asset Links file at \`/.well-known/assetlinks.json\`
|
|
8218
|
+
`
|
|
8219
|
+
};
|
|
8220
|
+
|
|
8221
|
+
// src/data/payments-guides.ts
|
|
8222
|
+
var paymentsGuides = {
|
|
8223
|
+
"idealyst://payments/overview": `# @idealyst/payments Overview
|
|
8224
|
+
|
|
8225
|
+
Cross-platform payment provider abstractions for React and React Native. Wraps Stripe's Platform Pay API for Apple Pay and Google Pay on mobile.
|
|
8226
|
+
|
|
8227
|
+
## Features
|
|
8228
|
+
|
|
8229
|
+
- **Apple Pay** \u2014 Native iOS payment sheet via Stripe SDK
|
|
8230
|
+
- **Google Pay** \u2014 Native Android payment sheet via Stripe SDK
|
|
8231
|
+
- **Cross-Platform** \u2014 Single API for React Native and web (web is stub/noop)
|
|
8232
|
+
- **Flat Functions + Hook** \u2014 Use \`initializePayments()\`, \`confirmPayment()\` directly, or \`usePayments()\` hook
|
|
8233
|
+
- **Stripe Platform Pay** \u2014 Wraps \`@stripe/stripe-react-native\` Platform Pay API
|
|
8234
|
+
- **Graceful Degradation** \u2014 Falls back cleanly when Stripe SDK isn't installed
|
|
8235
|
+
- **TypeScript** \u2014 Full type safety
|
|
8236
|
+
|
|
8237
|
+
## Installation
|
|
8238
|
+
|
|
8239
|
+
\`\`\`bash
|
|
8240
|
+
yarn add @idealyst/payments
|
|
8241
|
+
|
|
8242
|
+
# React Native \u2014 required for mobile payments:
|
|
8243
|
+
yarn add @stripe/stripe-react-native
|
|
8244
|
+
cd ios && pod install
|
|
8245
|
+
\`\`\`
|
|
8246
|
+
|
|
8247
|
+
## Web Support
|
|
8248
|
+
|
|
8249
|
+
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\`).
|
|
8250
|
+
|
|
8251
|
+
## Quick Start
|
|
8252
|
+
|
|
8253
|
+
\`\`\`tsx
|
|
8254
|
+
import { usePayments } from '@idealyst/payments';
|
|
8255
|
+
|
|
8256
|
+
function CheckoutScreen() {
|
|
8257
|
+
const {
|
|
8258
|
+
isReady,
|
|
8259
|
+
isApplePayAvailable,
|
|
8260
|
+
isGooglePayAvailable,
|
|
8261
|
+
isProcessing,
|
|
8262
|
+
confirmPayment,
|
|
8263
|
+
} = usePayments({
|
|
8264
|
+
config: {
|
|
8265
|
+
publishableKey: 'pk_test_xxx',
|
|
8266
|
+
merchantIdentifier: 'merchant.com.myapp',
|
|
8267
|
+
merchantName: 'My App',
|
|
8268
|
+
merchantCountryCode: 'US',
|
|
8269
|
+
},
|
|
8270
|
+
});
|
|
8271
|
+
|
|
8272
|
+
const handlePay = async () => {
|
|
8273
|
+
const result = await confirmPayment({
|
|
8274
|
+
clientSecret: 'pi_xxx_secret_xxx', // from server
|
|
8275
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
8276
|
+
});
|
|
8277
|
+
console.log('Paid:', result.paymentIntentId);
|
|
8278
|
+
};
|
|
8279
|
+
|
|
8280
|
+
if (!isReady) return null;
|
|
8281
|
+
|
|
8282
|
+
return (
|
|
8283
|
+
<Button
|
|
8284
|
+
onPress={handlePay}
|
|
8285
|
+
disabled={isProcessing}
|
|
8286
|
+
label={isApplePayAvailable ? 'Apple Pay' : 'Google Pay'}
|
|
8287
|
+
/>
|
|
8288
|
+
);
|
|
8289
|
+
}
|
|
8290
|
+
\`\`\`
|
|
8291
|
+
|
|
8292
|
+
## Platform Support
|
|
8293
|
+
|
|
8294
|
+
| Feature | iOS | Android | Web |
|
|
8295
|
+
|---------|-----|---------|-----|
|
|
8296
|
+
| Apple Pay | Yes | \u2014 | \u2014 |
|
|
8297
|
+
| Google Pay | \u2014 | Yes | \u2014 |
|
|
8298
|
+
| usePayments hook | Yes | Yes | Stub |
|
|
8299
|
+
| confirmPayment | Yes | Yes | Throws |
|
|
8300
|
+
| createPaymentMethod | Yes | Yes | Throws |
|
|
8301
|
+
`,
|
|
8302
|
+
"idealyst://payments/api": `# Payments API Reference
|
|
8303
|
+
|
|
8304
|
+
Complete API reference for @idealyst/payments.
|
|
8305
|
+
|
|
8306
|
+
---
|
|
8307
|
+
|
|
8308
|
+
## Flat Functions
|
|
8309
|
+
|
|
8310
|
+
### initializePayments
|
|
8311
|
+
|
|
8312
|
+
Initialize the Stripe SDK for platform payments.
|
|
8313
|
+
|
|
8314
|
+
\`\`\`tsx
|
|
8315
|
+
import { initializePayments } from '@idealyst/payments';
|
|
8316
|
+
|
|
8317
|
+
await initializePayments({
|
|
8318
|
+
publishableKey: 'pk_test_xxx',
|
|
8319
|
+
merchantIdentifier: 'merchant.com.myapp', // iOS Apple Pay
|
|
8320
|
+
merchantName: 'My App',
|
|
8321
|
+
merchantCountryCode: 'US',
|
|
8322
|
+
urlScheme: 'com.myapp', // for 3D Secure redirects
|
|
8323
|
+
testEnvironment: true, // Google Pay test mode
|
|
8324
|
+
});
|
|
8325
|
+
\`\`\`
|
|
8326
|
+
|
|
8327
|
+
**PaymentConfig:**
|
|
8328
|
+
|
|
8329
|
+
| Option | Type | Required | Description |
|
|
8330
|
+
|--------|------|----------|-------------|
|
|
8331
|
+
| publishableKey | string | Yes | Stripe publishable key |
|
|
8332
|
+
| merchantIdentifier | string | No | Apple Pay merchant ID (iOS) |
|
|
8333
|
+
| merchantName | string | Yes | Display name on payment sheet |
|
|
8334
|
+
| merchantCountryCode | string | Yes | ISO 3166-1 alpha-2 country |
|
|
8335
|
+
| urlScheme | string | No | URL scheme for 3D Secure |
|
|
8336
|
+
| testEnvironment | boolean | No | Google Pay test mode (default: false) |
|
|
8337
|
+
|
|
8338
|
+
### checkPaymentAvailability
|
|
8339
|
+
|
|
8340
|
+
Check which payment methods are available on the current device.
|
|
8341
|
+
|
|
8342
|
+
\`\`\`tsx
|
|
8343
|
+
import { checkPaymentAvailability } from '@idealyst/payments';
|
|
8344
|
+
|
|
8345
|
+
const methods = await checkPaymentAvailability();
|
|
8346
|
+
// [
|
|
8347
|
+
// { type: 'apple_pay', isAvailable: true },
|
|
8348
|
+
// { type: 'google_pay', isAvailable: false, unavailableReason: '...' },
|
|
8349
|
+
// { type: 'card', isAvailable: true },
|
|
8350
|
+
// ]
|
|
8351
|
+
\`\`\`
|
|
8352
|
+
|
|
8353
|
+
### confirmPayment
|
|
8354
|
+
|
|
8355
|
+
Present the platform payment sheet and confirm a PaymentIntent. Requires a \`clientSecret\` from a server-created PaymentIntent.
|
|
8356
|
+
|
|
8357
|
+
\`\`\`tsx
|
|
8358
|
+
import { confirmPayment } from '@idealyst/payments';
|
|
8359
|
+
|
|
8360
|
+
const result = await confirmPayment({
|
|
8361
|
+
clientSecret: 'pi_xxx_secret_xxx',
|
|
8362
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
8363
|
+
lineItems: [
|
|
8364
|
+
{ label: 'Widget', amount: 999 },
|
|
8365
|
+
{ label: 'Tax', amount: 100 },
|
|
8366
|
+
],
|
|
8367
|
+
billingAddress: {
|
|
8368
|
+
isRequired: true,
|
|
8369
|
+
format: 'FULL',
|
|
8370
|
+
},
|
|
8371
|
+
});
|
|
8372
|
+
|
|
8373
|
+
console.log(result.paymentIntentId); // 'pi_xxx'
|
|
8374
|
+
console.log(result.status); // 'succeeded'
|
|
8375
|
+
\`\`\`
|
|
8376
|
+
|
|
8377
|
+
### createPaymentMethod
|
|
8378
|
+
|
|
8379
|
+
Present the payment sheet and create a payment method without confirming. Returns a payment method ID for server-side processing.
|
|
8380
|
+
|
|
8381
|
+
\`\`\`tsx
|
|
8382
|
+
import { createPaymentMethod } from '@idealyst/payments';
|
|
8383
|
+
|
|
8384
|
+
const result = await createPaymentMethod({
|
|
8385
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
8386
|
+
});
|
|
8387
|
+
|
|
8388
|
+
console.log(result.paymentMethodId); // 'pm_xxx'
|
|
8389
|
+
// Send to your server for processing
|
|
8390
|
+
\`\`\`
|
|
8391
|
+
|
|
8392
|
+
### getPaymentStatus
|
|
8393
|
+
|
|
8394
|
+
Get the current payment provider status.
|
|
8395
|
+
|
|
8396
|
+
\`\`\`tsx
|
|
8397
|
+
import { getPaymentStatus } from '@idealyst/payments';
|
|
8398
|
+
|
|
8399
|
+
const status = getPaymentStatus();
|
|
8400
|
+
// { state: 'ready', availablePaymentMethods: [...], isPaymentAvailable: true }
|
|
8401
|
+
\`\`\`
|
|
8402
|
+
|
|
8403
|
+
---
|
|
8404
|
+
|
|
8405
|
+
## usePayments Hook
|
|
8406
|
+
|
|
8407
|
+
Convenience hook that wraps the flat functions with React state management.
|
|
8408
|
+
|
|
8409
|
+
\`\`\`tsx
|
|
8410
|
+
import { usePayments } from '@idealyst/payments';
|
|
8411
|
+
|
|
8412
|
+
const {
|
|
8413
|
+
// State
|
|
8414
|
+
status, // PaymentProviderStatus
|
|
8415
|
+
isReady, // boolean
|
|
8416
|
+
isProcessing, // boolean
|
|
8417
|
+
availablePaymentMethods, // PaymentMethodAvailability[]
|
|
8418
|
+
isApplePayAvailable, // boolean
|
|
8419
|
+
isGooglePayAvailable,// boolean
|
|
8420
|
+
isPaymentAvailable, // boolean
|
|
8421
|
+
error, // PaymentError | null
|
|
8422
|
+
|
|
8423
|
+
// Actions
|
|
8424
|
+
initialize, // (config: PaymentConfig) => Promise<void>
|
|
8425
|
+
checkAvailability, // () => Promise<PaymentMethodAvailability[]>
|
|
8426
|
+
confirmPayment, // (request: PaymentSheetRequest) => Promise<PaymentResult>
|
|
8427
|
+
createPaymentMethod, // (request: PaymentSheetRequest) => Promise<PaymentResult>
|
|
8428
|
+
clearError, // () => void
|
|
8429
|
+
} = usePayments(options?);
|
|
8430
|
+
\`\`\`
|
|
8431
|
+
|
|
8432
|
+
**UsePaymentsOptions:**
|
|
8433
|
+
|
|
8434
|
+
| Option | Type | Default | Description |
|
|
8435
|
+
|--------|------|---------|-------------|
|
|
8436
|
+
| config | PaymentConfig | \u2014 | Auto-initialize with this config |
|
|
8437
|
+
| autoCheckAvailability | boolean | true | Check availability after init |
|
|
8438
|
+
|
|
8439
|
+
---
|
|
8440
|
+
|
|
8441
|
+
## Types
|
|
8442
|
+
|
|
8443
|
+
### PaymentMethodType
|
|
8444
|
+
|
|
8445
|
+
\`\`\`tsx
|
|
8446
|
+
type PaymentMethodType = 'apple_pay' | 'google_pay' | 'card';
|
|
8447
|
+
\`\`\`
|
|
8448
|
+
|
|
8449
|
+
### PaymentAmount
|
|
8450
|
+
|
|
8451
|
+
\`\`\`tsx
|
|
8452
|
+
interface PaymentAmount {
|
|
8453
|
+
amount: number; // smallest currency unit (cents)
|
|
8454
|
+
currencyCode: string; // ISO 4217 (e.g., 'usd')
|
|
8455
|
+
}
|
|
8456
|
+
\`\`\`
|
|
8457
|
+
|
|
8458
|
+
### PaymentSheetRequest
|
|
8459
|
+
|
|
8460
|
+
\`\`\`tsx
|
|
8461
|
+
interface PaymentSheetRequest {
|
|
8462
|
+
clientSecret?: string; // required for confirmPayment
|
|
8463
|
+
amount: PaymentAmount;
|
|
8464
|
+
lineItems?: PaymentLineItem[];
|
|
8465
|
+
billingAddress?: BillingAddressConfig;
|
|
8466
|
+
allowedPaymentMethods?: PaymentMethodType[];
|
|
8467
|
+
}
|
|
8468
|
+
\`\`\`
|
|
8469
|
+
|
|
8470
|
+
### PaymentResult
|
|
8471
|
+
|
|
8472
|
+
\`\`\`tsx
|
|
8473
|
+
interface PaymentResult {
|
|
8474
|
+
paymentMethodType: PaymentMethodType;
|
|
8475
|
+
paymentMethodId?: string; // for createPaymentMethod
|
|
8476
|
+
paymentIntentId?: string; // for confirmPayment
|
|
8477
|
+
status?: string; // 'succeeded', 'requires_capture', etc.
|
|
8478
|
+
}
|
|
8479
|
+
\`\`\`
|
|
8480
|
+
|
|
8481
|
+
### PaymentError
|
|
6755
8482
|
|
|
6756
8483
|
\`\`\`tsx
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
8484
|
+
interface PaymentError {
|
|
8485
|
+
code: PaymentErrorCode;
|
|
8486
|
+
message: string;
|
|
8487
|
+
originalError?: unknown;
|
|
8488
|
+
}
|
|
8489
|
+
|
|
8490
|
+
type PaymentErrorCode =
|
|
8491
|
+
| 'not_initialized'
|
|
8492
|
+
| 'not_available'
|
|
8493
|
+
| 'not_supported'
|
|
8494
|
+
| 'user_cancelled'
|
|
8495
|
+
| 'payment_failed'
|
|
8496
|
+
| 'network_error'
|
|
8497
|
+
| 'invalid_config'
|
|
8498
|
+
| 'invalid_request'
|
|
8499
|
+
| 'provider_error'
|
|
8500
|
+
| 'unknown';
|
|
8501
|
+
\`\`\`
|
|
8502
|
+
`,
|
|
8503
|
+
"idealyst://payments/examples": `# Payments Examples
|
|
6760
8504
|
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
8505
|
+
Complete code examples for common @idealyst/payments patterns.
|
|
8506
|
+
|
|
8507
|
+
## Basic Checkout with Hook
|
|
8508
|
+
|
|
8509
|
+
\`\`\`tsx
|
|
8510
|
+
import { usePayments } from '@idealyst/payments';
|
|
8511
|
+
import { Button, View, Text } from '@idealyst/components';
|
|
8512
|
+
|
|
8513
|
+
function CheckoutScreen({ clientSecret }: { clientSecret: string }) {
|
|
8514
|
+
const {
|
|
8515
|
+
isReady,
|
|
8516
|
+
isApplePayAvailable,
|
|
8517
|
+
isGooglePayAvailable,
|
|
8518
|
+
isProcessing,
|
|
8519
|
+
error,
|
|
8520
|
+
confirmPayment,
|
|
8521
|
+
clearError,
|
|
8522
|
+
} = usePayments({
|
|
8523
|
+
config: {
|
|
8524
|
+
publishableKey: 'pk_test_xxx',
|
|
8525
|
+
merchantIdentifier: 'merchant.com.myapp',
|
|
8526
|
+
merchantName: 'My App',
|
|
8527
|
+
merchantCountryCode: 'US',
|
|
8528
|
+
testEnvironment: __DEV__,
|
|
8529
|
+
},
|
|
8530
|
+
});
|
|
8531
|
+
|
|
8532
|
+
const handlePay = async () => {
|
|
8533
|
+
try {
|
|
8534
|
+
const result = await confirmPayment({
|
|
8535
|
+
clientSecret,
|
|
8536
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
8537
|
+
lineItems: [
|
|
8538
|
+
{ label: 'Widget', amount: 999 },
|
|
8539
|
+
{ label: 'Tax', amount: 100 },
|
|
8540
|
+
],
|
|
8541
|
+
});
|
|
8542
|
+
|
|
8543
|
+
// Payment succeeded
|
|
8544
|
+
console.log('Payment confirmed:', result.paymentIntentId);
|
|
8545
|
+
} catch (err) {
|
|
8546
|
+
// Error is automatically set in hook state
|
|
8547
|
+
console.error('Payment failed:', err);
|
|
8548
|
+
}
|
|
8549
|
+
};
|
|
8550
|
+
|
|
8551
|
+
if (!isReady) {
|
|
8552
|
+
return <Text>Loading payment methods...</Text>;
|
|
8553
|
+
}
|
|
8554
|
+
|
|
8555
|
+
const canPay = isApplePayAvailable || isGooglePayAvailable;
|
|
6786
8556
|
|
|
6787
|
-
function ComparisonChart() {
|
|
6788
8557
|
return (
|
|
6789
|
-
<
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
8558
|
+
<View>
|
|
8559
|
+
{canPay ? (
|
|
8560
|
+
<Button
|
|
8561
|
+
onPress={handlePay}
|
|
8562
|
+
disabled={isProcessing}
|
|
8563
|
+
intent="primary"
|
|
8564
|
+
>
|
|
8565
|
+
{isApplePayAvailable ? 'Pay with Apple Pay' : 'Pay with Google Pay'}
|
|
8566
|
+
</Button>
|
|
8567
|
+
) : (
|
|
8568
|
+
<Text>No payment methods available</Text>
|
|
8569
|
+
)}
|
|
8570
|
+
|
|
8571
|
+
{error && (
|
|
8572
|
+
<View>
|
|
8573
|
+
<Text intent="danger">{error.message}</Text>
|
|
8574
|
+
<Button onPress={clearError}>Dismiss</Button>
|
|
8575
|
+
</View>
|
|
8576
|
+
)}
|
|
8577
|
+
</View>
|
|
6796
8578
|
);
|
|
6797
8579
|
}
|
|
6798
8580
|
\`\`\`
|
|
6799
8581
|
|
|
6800
|
-
##
|
|
8582
|
+
## Create Payment Method (Server-Side Confirm)
|
|
6801
8583
|
|
|
6802
8584
|
\`\`\`tsx
|
|
6803
|
-
import
|
|
6804
|
-
|
|
6805
|
-
|
|
8585
|
+
import { usePayments } from '@idealyst/payments';
|
|
8586
|
+
|
|
8587
|
+
function DonateScreen() {
|
|
8588
|
+
const { isReady, isPaymentAvailable, createPaymentMethod } = usePayments({
|
|
8589
|
+
config: {
|
|
8590
|
+
publishableKey: 'pk_test_xxx',
|
|
8591
|
+
merchantName: 'Charity',
|
|
8592
|
+
merchantCountryCode: 'US',
|
|
8593
|
+
},
|
|
8594
|
+
});
|
|
6806
8595
|
|
|
6807
|
-
const
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
8596
|
+
const handleDonate = async (amountCents: number) => {
|
|
8597
|
+
const result = await createPaymentMethod({
|
|
8598
|
+
amount: { amount: amountCents, currencyCode: 'usd' },
|
|
8599
|
+
});
|
|
8600
|
+
|
|
8601
|
+
// Send payment method to your server
|
|
8602
|
+
await fetch('/api/donate', {
|
|
8603
|
+
method: 'POST',
|
|
8604
|
+
body: JSON.stringify({
|
|
8605
|
+
paymentMethodId: result.paymentMethodId,
|
|
8606
|
+
amount: amountCents,
|
|
8607
|
+
}),
|
|
8608
|
+
});
|
|
8609
|
+
};
|
|
6814
8610
|
|
|
6815
|
-
function CategoryBreakdown() {
|
|
6816
8611
|
return (
|
|
6817
|
-
<View
|
|
6818
|
-
<
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
yAxis={{ tickFormat: (value: number | string | Date) => \`\${value} units\` }}
|
|
6825
|
-
/>
|
|
8612
|
+
<View>
|
|
8613
|
+
<Button onPress={() => handleDonate(500)} disabled={!isPaymentAvailable}>
|
|
8614
|
+
Donate $5
|
|
8615
|
+
</Button>
|
|
8616
|
+
<Button onPress={() => handleDonate(1000)} disabled={!isPaymentAvailable}>
|
|
8617
|
+
Donate $10
|
|
8618
|
+
</Button>
|
|
6826
8619
|
</View>
|
|
6827
8620
|
);
|
|
6828
8621
|
}
|
|
6829
8622
|
\`\`\`
|
|
6830
8623
|
|
|
6831
|
-
##
|
|
8624
|
+
## Flat Functions (No Hook)
|
|
6832
8625
|
|
|
6833
8626
|
\`\`\`tsx
|
|
6834
|
-
import
|
|
6835
|
-
|
|
8627
|
+
import {
|
|
8628
|
+
initializePayments,
|
|
8629
|
+
checkPaymentAvailability,
|
|
8630
|
+
confirmPayment,
|
|
8631
|
+
getPaymentStatus,
|
|
8632
|
+
} from '@idealyst/payments';
|
|
8633
|
+
import type { PaymentError } from '@idealyst/payments';
|
|
8634
|
+
|
|
8635
|
+
// Initialize once at app startup
|
|
8636
|
+
async function setupPayments() {
|
|
8637
|
+
await initializePayments({
|
|
8638
|
+
publishableKey: 'pk_test_xxx',
|
|
8639
|
+
merchantIdentifier: 'merchant.com.myapp',
|
|
8640
|
+
merchantName: 'My App',
|
|
8641
|
+
merchantCountryCode: 'US',
|
|
8642
|
+
});
|
|
6836
8643
|
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
height={300}
|
|
6863
|
-
stacked
|
|
6864
|
-
animate
|
|
6865
|
-
/>
|
|
6866
|
-
);
|
|
8644
|
+
const status = getPaymentStatus();
|
|
8645
|
+
console.log('Payment ready:', status.state === 'ready');
|
|
8646
|
+
}
|
|
8647
|
+
|
|
8648
|
+
// Check availability
|
|
8649
|
+
async function canPay() {
|
|
8650
|
+
const methods = await checkPaymentAvailability();
|
|
8651
|
+
return methods.some(m => m.isAvailable);
|
|
8652
|
+
}
|
|
8653
|
+
|
|
8654
|
+
// Process payment
|
|
8655
|
+
async function processPayment(clientSecret: string, totalCents: number) {
|
|
8656
|
+
try {
|
|
8657
|
+
const result = await confirmPayment({
|
|
8658
|
+
clientSecret,
|
|
8659
|
+
amount: { amount: totalCents, currencyCode: 'usd' },
|
|
8660
|
+
});
|
|
8661
|
+
return result;
|
|
8662
|
+
} catch (err) {
|
|
8663
|
+
const paymentErr = err as PaymentError;
|
|
8664
|
+
if (paymentErr.code === 'user_cancelled') {
|
|
8665
|
+
return null; // User cancelled \u2014 not an error
|
|
8666
|
+
}
|
|
8667
|
+
throw paymentErr;
|
|
8668
|
+
}
|
|
6867
8669
|
}
|
|
6868
8670
|
\`\`\`
|
|
6869
8671
|
|
|
6870
|
-
##
|
|
8672
|
+
## Platform-Conditional UI
|
|
6871
8673
|
|
|
6872
8674
|
\`\`\`tsx
|
|
6873
|
-
import
|
|
6874
|
-
import {
|
|
8675
|
+
import { usePayments } from '@idealyst/payments';
|
|
8676
|
+
import { Platform } from 'react-native';
|
|
6875
8677
|
|
|
6876
|
-
function
|
|
6877
|
-
const
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
8678
|
+
function PaymentButtons({ clientSecret }: { clientSecret: string }) {
|
|
8679
|
+
const {
|
|
8680
|
+
isApplePayAvailable,
|
|
8681
|
+
isGooglePayAvailable,
|
|
8682
|
+
confirmPayment,
|
|
8683
|
+
isProcessing,
|
|
8684
|
+
} = usePayments({
|
|
8685
|
+
config: {
|
|
8686
|
+
publishableKey: 'pk_test_xxx',
|
|
8687
|
+
merchantName: 'My Store',
|
|
8688
|
+
merchantCountryCode: 'US',
|
|
8689
|
+
},
|
|
8690
|
+
});
|
|
8691
|
+
|
|
8692
|
+
const handlePlatformPay = () =>
|
|
8693
|
+
confirmPayment({
|
|
8694
|
+
clientSecret,
|
|
8695
|
+
amount: { amount: 2499, currencyCode: 'usd' },
|
|
8696
|
+
});
|
|
6883
8697
|
|
|
6884
8698
|
return (
|
|
6885
|
-
<
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
8699
|
+
<View>
|
|
8700
|
+
{isApplePayAvailable && (
|
|
8701
|
+
<Button
|
|
8702
|
+
onPress={handlePlatformPay}
|
|
8703
|
+
disabled={isProcessing}
|
|
8704
|
+
iconName="apple"
|
|
8705
|
+
>
|
|
8706
|
+
Apple Pay
|
|
8707
|
+
</Button>
|
|
8708
|
+
)}
|
|
8709
|
+
|
|
8710
|
+
{isGooglePayAvailable && (
|
|
8711
|
+
<Button
|
|
8712
|
+
onPress={handlePlatformPay}
|
|
8713
|
+
disabled={isProcessing}
|
|
8714
|
+
iconName="google"
|
|
8715
|
+
>
|
|
8716
|
+
Google Pay
|
|
8717
|
+
</Button>
|
|
8718
|
+
)}
|
|
8719
|
+
|
|
8720
|
+
{/* Always show a card fallback */}
|
|
8721
|
+
<Button
|
|
8722
|
+
onPress={() => navigateToCardForm()}
|
|
8723
|
+
intent="secondary"
|
|
8724
|
+
>
|
|
8725
|
+
Pay with Card
|
|
8726
|
+
</Button>
|
|
8727
|
+
</View>
|
|
6891
8728
|
);
|
|
6892
8729
|
}
|
|
6893
8730
|
\`\`\`
|
|
8731
|
+
|
|
8732
|
+
## Best Practices
|
|
8733
|
+
|
|
8734
|
+
1. **Server-side PaymentIntent** \u2014 Always create PaymentIntents on your server, never on the client
|
|
8735
|
+
2. **Handle cancellation** \u2014 \`user_cancelled\` is not an error, don't show error UI for it
|
|
8736
|
+
3. **Test environment** \u2014 Set \`testEnvironment: true\` during development for Google Pay
|
|
8737
|
+
4. **Apple Pay merchant ID** \u2014 Requires Apple Developer Program and Xcode capability setup
|
|
8738
|
+
5. **Amounts in cents** \u2014 All amounts are in the smallest currency unit (1099 = $10.99)
|
|
8739
|
+
6. **Web fallback** \u2014 On web, use \`@stripe/react-stripe-js\` (Stripe Elements) directly
|
|
8740
|
+
7. **3D Secure** \u2014 Set \`urlScheme\` in config for 3D Secure / bank redirect flows on native
|
|
8741
|
+
8. **Error handling** \u2014 Always wrap \`confirmPayment\` / \`createPaymentMethod\` in try/catch
|
|
6894
8742
|
`
|
|
6895
8743
|
};
|
|
6896
8744
|
|
|
@@ -7811,6 +9659,156 @@ function UploadForm() {
|
|
|
7811
9659
|
"Use customMimeTypes for specific formats like 'application/pdf'"
|
|
7812
9660
|
],
|
|
7813
9661
|
relatedPackages: ["components", "camera", "storage"]
|
|
9662
|
+
},
|
|
9663
|
+
clipboard: {
|
|
9664
|
+
name: "Clipboard",
|
|
9665
|
+
npmName: "@idealyst/clipboard",
|
|
9666
|
+
description: "Cross-platform clipboard and OTP autofill for React and React Native. Copy/paste text and auto-detect SMS verification codes on mobile.",
|
|
9667
|
+
category: "utility",
|
|
9668
|
+
platforms: ["web", "native"],
|
|
9669
|
+
documentationStatus: "full",
|
|
9670
|
+
installation: "yarn add @idealyst/clipboard",
|
|
9671
|
+
peerDependencies: [
|
|
9672
|
+
"@react-native-clipboard/clipboard (native)",
|
|
9673
|
+
"react-native-otp-verify (Android OTP, optional)"
|
|
9674
|
+
],
|
|
9675
|
+
features: [
|
|
9676
|
+
"Cross-platform copy/paste with async API",
|
|
9677
|
+
"Android SMS OTP auto-read via SMS Retriever API (no permissions)",
|
|
9678
|
+
"iOS OTP keyboard autofill via TextInput props",
|
|
9679
|
+
"Clipboard change listeners",
|
|
9680
|
+
"Graceful degradation when native modules missing"
|
|
9681
|
+
],
|
|
9682
|
+
quickStart: `import { clipboard } from '@idealyst/clipboard';
|
|
9683
|
+
|
|
9684
|
+
// Copy text
|
|
9685
|
+
await clipboard.copy('Hello, world!');
|
|
9686
|
+
|
|
9687
|
+
// Paste text
|
|
9688
|
+
const text = await clipboard.paste();
|
|
9689
|
+
|
|
9690
|
+
// OTP auto-fill (mobile)
|
|
9691
|
+
import { useOTPAutoFill, OTP_INPUT_PROPS } from '@idealyst/clipboard';
|
|
9692
|
+
|
|
9693
|
+
function OTPScreen() {
|
|
9694
|
+
const { code, startListening } = useOTPAutoFill({
|
|
9695
|
+
codeLength: 6,
|
|
9696
|
+
onCodeReceived: (otp) => verifyOTP(otp),
|
|
9697
|
+
});
|
|
9698
|
+
|
|
9699
|
+
useEffect(() => { startListening(); }, []);
|
|
9700
|
+
|
|
9701
|
+
return <TextInput value={code ?? ''} {...OTP_INPUT_PROPS} />;
|
|
9702
|
+
}`,
|
|
9703
|
+
apiHighlights: [
|
|
9704
|
+
"clipboard.copy(text) - Copy text to clipboard",
|
|
9705
|
+
"clipboard.paste() - Read text from clipboard",
|
|
9706
|
+
"clipboard.hasText() - Check if clipboard has text",
|
|
9707
|
+
"clipboard.addListener(fn) - Listen for copy events",
|
|
9708
|
+
"useOTPAutoFill({ codeLength, onCodeReceived }) - Auto-detect SMS OTP codes (Android)",
|
|
9709
|
+
"OTP_INPUT_PROPS - TextInput props for iOS OTP keyboard autofill"
|
|
9710
|
+
],
|
|
9711
|
+
relatedPackages: ["storage", "components"]
|
|
9712
|
+
},
|
|
9713
|
+
biometrics: {
|
|
9714
|
+
name: "Biometrics",
|
|
9715
|
+
npmName: "@idealyst/biometrics",
|
|
9716
|
+
description: "Cross-platform biometric authentication and passkeys (WebAuthn/FIDO2) for React and React Native. FaceID, TouchID, fingerprint, iris, and passwordless login.",
|
|
9717
|
+
category: "auth",
|
|
9718
|
+
platforms: ["web", "native"],
|
|
9719
|
+
documentationStatus: "full",
|
|
9720
|
+
installation: "yarn add @idealyst/biometrics",
|
|
9721
|
+
peerDependencies: [
|
|
9722
|
+
"expo-local-authentication (native biometrics)",
|
|
9723
|
+
"react-native-passkeys (native passkeys, optional)"
|
|
9724
|
+
],
|
|
9725
|
+
features: [
|
|
9726
|
+
"Local biometric auth \u2014 FaceID, TouchID, fingerprint, iris",
|
|
9727
|
+
"Passkeys (WebAuthn/FIDO2) \u2014 passwordless login with cryptographic credentials",
|
|
9728
|
+
"Cross-platform \u2014 single API for web and React Native",
|
|
9729
|
+
"Web biometrics via WebAuthn userVerification",
|
|
9730
|
+
"Web passkeys via navigator.credentials",
|
|
9731
|
+
"Graceful degradation when native modules missing",
|
|
9732
|
+
"Base64url encoding helpers for WebAuthn data"
|
|
9733
|
+
],
|
|
9734
|
+
quickStart: `import { isBiometricAvailable, authenticate } from '@idealyst/biometrics';
|
|
9735
|
+
|
|
9736
|
+
// Check biometric availability
|
|
9737
|
+
const available = await isBiometricAvailable();
|
|
9738
|
+
|
|
9739
|
+
// Authenticate
|
|
9740
|
+
if (available) {
|
|
9741
|
+
const result = await authenticate({ promptMessage: 'Verify your identity' });
|
|
9742
|
+
if (result.success) {
|
|
9743
|
+
// Authenticated!
|
|
9744
|
+
}
|
|
9745
|
+
}
|
|
9746
|
+
|
|
9747
|
+
// Passkeys
|
|
9748
|
+
import { isPasskeySupported, createPasskey, getPasskey } from '@idealyst/biometrics';
|
|
9749
|
+
|
|
9750
|
+
const credential = await createPasskey({
|
|
9751
|
+
challenge: serverChallenge,
|
|
9752
|
+
rp: { id: 'example.com', name: 'My App' },
|
|
9753
|
+
user: { id: userId, name: email, displayName: name },
|
|
9754
|
+
});`,
|
|
9755
|
+
apiHighlights: [
|
|
9756
|
+
"isBiometricAvailable() - Check biometric hardware and enrollment",
|
|
9757
|
+
"getBiometricTypes() - Get available biometric types",
|
|
9758
|
+
"getSecurityLevel() - Get device security level",
|
|
9759
|
+
"authenticate(options) - Prompt for biometric auth",
|
|
9760
|
+
"cancelAuthentication() - Cancel auth (Android)",
|
|
9761
|
+
"isPasskeySupported() - Check passkey support",
|
|
9762
|
+
"createPasskey(options) - Register a new passkey",
|
|
9763
|
+
"getPasskey(options) - Authenticate with passkey"
|
|
9764
|
+
],
|
|
9765
|
+
relatedPackages: ["oauth-client", "storage"]
|
|
9766
|
+
},
|
|
9767
|
+
payments: {
|
|
9768
|
+
name: "Payments",
|
|
9769
|
+
npmName: "@idealyst/payments",
|
|
9770
|
+
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.",
|
|
9771
|
+
category: "utility",
|
|
9772
|
+
platforms: ["web", "native"],
|
|
9773
|
+
documentationStatus: "full",
|
|
9774
|
+
installation: "yarn add @idealyst/payments @stripe/stripe-react-native",
|
|
9775
|
+
peerDependencies: [
|
|
9776
|
+
"@stripe/stripe-react-native (native)"
|
|
9777
|
+
],
|
|
9778
|
+
features: [
|
|
9779
|
+
"Apple Pay via Stripe Platform Pay (iOS)",
|
|
9780
|
+
"Google Pay via Stripe Platform Pay (Android)",
|
|
9781
|
+
"Flat function API + usePayments convenience hook",
|
|
9782
|
+
"PaymentIntent confirmation flow",
|
|
9783
|
+
"Payment method creation flow",
|
|
9784
|
+
"Normalized error handling across platforms",
|
|
9785
|
+
"Graceful degradation when Stripe SDK missing",
|
|
9786
|
+
"Web stub with guidance to Stripe Elements"
|
|
9787
|
+
],
|
|
9788
|
+
quickStart: `import { usePayments } from '@idealyst/payments';
|
|
9789
|
+
|
|
9790
|
+
const { isReady, isApplePayAvailable, confirmPayment } = usePayments({
|
|
9791
|
+
config: {
|
|
9792
|
+
publishableKey: 'pk_test_xxx',
|
|
9793
|
+
merchantIdentifier: 'merchant.com.myapp',
|
|
9794
|
+
merchantName: 'My App',
|
|
9795
|
+
merchantCountryCode: 'US',
|
|
9796
|
+
},
|
|
9797
|
+
});
|
|
9798
|
+
|
|
9799
|
+
const result = await confirmPayment({
|
|
9800
|
+
clientSecret: 'pi_xxx_secret_xxx',
|
|
9801
|
+
amount: { amount: 1099, currencyCode: 'usd' },
|
|
9802
|
+
});`,
|
|
9803
|
+
apiHighlights: [
|
|
9804
|
+
"initializePayments(config) - Initialize Stripe SDK",
|
|
9805
|
+
"checkPaymentAvailability() - Check Apple Pay / Google Pay / card",
|
|
9806
|
+
"confirmPayment(request) - Present sheet and confirm PaymentIntent",
|
|
9807
|
+
"createPaymentMethod(request) - Present sheet and create payment method",
|
|
9808
|
+
"getPaymentStatus() - Get current provider status",
|
|
9809
|
+
"usePayments(options?) - Convenience hook with state management"
|
|
9810
|
+
],
|
|
9811
|
+
relatedPackages: ["biometrics", "oauth-client"]
|
|
7814
9812
|
}
|
|
7815
9813
|
};
|
|
7816
9814
|
function getPackagesByCategory() {
|
|
@@ -19909,7 +21907,7 @@ function getStorageGuide(args) {
|
|
|
19909
21907
|
const guide = storageGuides[uri];
|
|
19910
21908
|
if (!guide) {
|
|
19911
21909
|
return textResponse(
|
|
19912
|
-
`Topic "${topic}" not found. Available topics: overview, api, examples`
|
|
21910
|
+
`Topic "${topic}" not found. Available topics: overview, api, examples, secure`
|
|
19913
21911
|
);
|
|
19914
21912
|
}
|
|
19915
21913
|
return textResponse(guide);
|
|
@@ -20035,6 +22033,39 @@ function getChartsGuide(args) {
|
|
|
20035
22033
|
}
|
|
20036
22034
|
return textResponse(guide);
|
|
20037
22035
|
}
|
|
22036
|
+
function getClipboardGuide(args) {
|
|
22037
|
+
const topic = args.topic;
|
|
22038
|
+
const uri = `idealyst://clipboard/${topic}`;
|
|
22039
|
+
const guide = clipboardGuides[uri];
|
|
22040
|
+
if (!guide) {
|
|
22041
|
+
return textResponse(
|
|
22042
|
+
`Topic "${topic}" not found. Available topics: overview, api, examples`
|
|
22043
|
+
);
|
|
22044
|
+
}
|
|
22045
|
+
return textResponse(guide);
|
|
22046
|
+
}
|
|
22047
|
+
function getBiometricsGuide(args) {
|
|
22048
|
+
const topic = args.topic;
|
|
22049
|
+
const uri = `idealyst://biometrics/${topic}`;
|
|
22050
|
+
const guide = biometricsGuides[uri];
|
|
22051
|
+
if (!guide) {
|
|
22052
|
+
return textResponse(
|
|
22053
|
+
`Topic "${topic}" not found. Available topics: overview, api, examples`
|
|
22054
|
+
);
|
|
22055
|
+
}
|
|
22056
|
+
return textResponse(guide);
|
|
22057
|
+
}
|
|
22058
|
+
function getPaymentsGuide(args) {
|
|
22059
|
+
const topic = args.topic;
|
|
22060
|
+
const uri = `idealyst://payments/${topic}`;
|
|
22061
|
+
const guide = paymentsGuides[uri];
|
|
22062
|
+
if (!guide) {
|
|
22063
|
+
return textResponse(
|
|
22064
|
+
`Topic "${topic}" not found. Available topics: overview, api, examples`
|
|
22065
|
+
);
|
|
22066
|
+
}
|
|
22067
|
+
return textResponse(guide);
|
|
22068
|
+
}
|
|
20038
22069
|
function listPackages(args = {}) {
|
|
20039
22070
|
const category = args.category;
|
|
20040
22071
|
if (category) {
|
|
@@ -20280,6 +22311,9 @@ var toolHandlers = {
|
|
|
20280
22311
|
get_markdown_guide: getMarkdownGuide,
|
|
20281
22312
|
get_config_guide: getConfigGuide,
|
|
20282
22313
|
get_charts_guide: getChartsGuide,
|
|
22314
|
+
get_clipboard_guide: getClipboardGuide,
|
|
22315
|
+
get_biometrics_guide: getBiometricsGuide,
|
|
22316
|
+
get_payments_guide: getPaymentsGuide,
|
|
20283
22317
|
list_packages: listPackages,
|
|
20284
22318
|
get_package_docs: getPackageDocs,
|
|
20285
22319
|
search_packages: searchPackages2,
|
|
@@ -20304,12 +22338,16 @@ function callTool(name, args = {}) {
|
|
|
20304
22338
|
getAudioGuide,
|
|
20305
22339
|
getAudioGuideDefinition,
|
|
20306
22340
|
getAvailableComponents,
|
|
22341
|
+
getBiometricsGuide,
|
|
22342
|
+
getBiometricsGuideDefinition,
|
|
20307
22343
|
getCameraGuide,
|
|
20308
22344
|
getCameraGuideDefinition,
|
|
20309
22345
|
getChartsGuide,
|
|
20310
22346
|
getChartsGuideDefinition,
|
|
20311
22347
|
getCliUsage,
|
|
20312
22348
|
getCliUsageDefinition,
|
|
22349
|
+
getClipboardGuide,
|
|
22350
|
+
getClipboardGuideDefinition,
|
|
20313
22351
|
getComponentDocs,
|
|
20314
22352
|
getComponentDocsDefinition,
|
|
20315
22353
|
getComponentExample,
|
|
@@ -20341,6 +22379,8 @@ function callTool(name, args = {}) {
|
|
|
20341
22379
|
getOauthClientGuideDefinition,
|
|
20342
22380
|
getPackageDocs,
|
|
20343
22381
|
getPackageDocsDefinition,
|
|
22382
|
+
getPaymentsGuide,
|
|
22383
|
+
getPaymentsGuideDefinition,
|
|
20344
22384
|
getRecipe,
|
|
20345
22385
|
getRecipeDefinition,
|
|
20346
22386
|
getRegistryThemeValues,
|