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