React Native SDK for ZKP2P - A peer-to-peer fiat-to-crypto on/off-ramp powered by zero-knowledge proofs.
yarn add @zkp2p/zkp2p-react-native-sdk @react-native-async-storage/async-storage @react-native-cookies/cookies react-native-webview @zkp2p/webview-intercept viem react-native-svg react-native-device-infocd ios && pod installreact-native-device-info: Used for dynamic memory management in proof generationreact-native-svg: Required for animated UI components
The SDK supports two modes:
- Full Mode: Access all features including blockchain operations (requires wallet & API key)
- Proof-Only Mode: Generate proofs without wallet or API key
import { Zkp2pProvider, useZkp2p } from '@zkp2p/zkp2p-react-native-sdk';
import { createWalletClient, custom } from 'viem';
// 1. Setup wallet client
const walletClient = createWalletClient({
chain: base,
transport: custom(window.ethereum),
});
// 2. Wrap your app with Zkp2pProvider
function App() {
return (
<Zkp2pProvider
walletClient={walletClient}
apiKey="your-api-key"
chainId={8453} // Base
prover="reclaim_gnark" // or "reclaim_snarkjs"
rpcUrl="https://base-mainnet.g.alchemy.com/v2/your-key" // explicit RPC (recommended)
>
<YourApp />
</Zkp2pProvider>
);
}// No wallet or API key required!
function App() {
return (
<Zkp2pProvider
chainId={8453} // Base
prover="reclaim_gnark"
// rpcUrl is optional here too if you want to pin to a specific endpoint
// rpcUrl="https://base-sepolia.g.alchemy.com/v2/your-key"
>
<YourApp />
</Zkp2pProvider>
);
}
// 3. Use the SDK in your components
function PaymentFlow() {
const {
flowState,
initiate,
authenticate,
generateProof,
zkp2pClient, // null in proof-only mode
proofData,
metadataList,
proofStatus,
} = useZkp2p();
// Check mode
const isProofOnlyMode = !zkp2pClient;
const isProofRunning = proofStatus.phase === 'running';
// Your component logic
}| Prop | Type | Default | Description |
|---|---|---|---|
walletClient |
WalletClient |
Optional | Viem wallet client for blockchain interactions (required for full mode) |
apiKey |
string |
Optional | Your ZKP2P API key (required for full mode) |
chainId |
number |
8453 |
Blockchain chain ID (8453 for Base, 31337 for Hardhat) |
environment |
'production' | 'staging' |
'production' |
Environment (production or staging) |
prover |
'reclaim_snarkjs' | 'reclaim_gnark' |
'reclaim_gnark' |
Proof generation method |
witnessUrl |
string |
'https://witness-proxy.zkp2p.xyz' |
Witness server URL |
baseApiUrl |
string |
'https://api.zkp2p.xyz' |
ZKP2P API base URL (versioned paths are appended by the SDK) |
rpcUrl |
string |
Optional | HTTP RPC endpoint for the selected chain (e.g., Alchemy/Infura/Base). If omitted, viem uses the chain default. |
rpcTimeout |
number |
30000 |
RPC timeout in milliseconds |
configBaseUrl |
string |
'https://raw.githubusercontent.com/zkp2p/providers/main/' |
Provider configuration base URL |
storage |
Storage |
Optional | App-provided secure storage instance (e.g., SecureStore). Required to persist credentials/consent. |
hideDefaultProofUI |
boolean |
false |
When true, the SDK does not render its proof progress sheet. Use this for headless/custom proof animations. |
renderConsentSheet |
(props) => ReactNode |
Optional | If provided, SDK renders this after visible login when consent is unset. Your component must call onAccept / onSkip / onDeny. |
Signal your intent to buy/sell crypto. This must be called before initiating the payment flow.
const { zkp2pClient } = useZkp2p();
const signalArgs = {
depositId: '123', // The deposit ID you want to fulfill
amount: '100', // Amount in USD
// ... other contract parameters
};
const tx = await zkp2pClient.signalIntent(signalArgs);Start the payment proof generation flow. Opens payment provider authentication.
const { initiate } = useZkp2p();
const provider = await initiate('venmo', 'transfer_venmo', {
// Optional: Auto-start with payment action
initialAction: {
enabled: true,
paymentDetails: {
RECIPIENT_ID: 'john-doe-123',
AMOUNT: '100'
}
}
});Generate zero-knowledge proof(s) for a specific transaction. Always returns an array of ProofData.
const { generateProof, provider, interceptedPayload } = useZkp2p();
try {
const proofs = await generateProof(
provider,
interceptedPayload,
'0x...', // Intent hash from signalIntent
0 // Transaction index to prove
);
console.log('Proofs generated:', proofs);
} catch (error) {
console.error('Proof generation failed:', error);
// Fallback to manual authentication
await authenticate('venmo', 'transfer_venmo');
}Start the authentication flow for a specific provider. Useful for manual flows or when you want to drive the UI yourself.
const { authenticate } = useZkp2p();
// Simple authentication without auto-proof
await authenticate('venmo', 'transfer_venmo');
// Or with auto-proof generation
await authenticate('venmo', 'transfer_venmo', {
autoGenerateProof: {
intentHash: '0x...',
itemIndex: 0,
onProofGenerated: (proofData) => {
// Handle generated proof
},
onProofError: (err) => console.error(err),
},
});
Note: Both `initiate(...)` and `authenticate(...)` start a new session and clear `proofData`, `metadataList`, and `interceptedPayload` to ensure no stale state carries into the new flow.
### Passing a custom RPC without the Provider
If you instantiate the client directly, pass `rpcUrl` on the options:
```ts
import { Zkp2pClient } from '@zkp2p/zkp2p-react-native-sdk';
import { base } from 'viem/chains';
const client = new Zkp2pClient({
prover: 'reclaim_gnark',
chainId: base.id,
apiKey: 'your-api-key',
rpcUrl: 'https://base-mainnet.g.alchemy.com/v2/your-key',
});The SDK can store credentials (username/password) per provider/action when the user consents. You supply:
storage: aStorageimplementation (e.g., backed by SecureStore or AsyncStorage) viaZkp2pProvider.renderConsentSheet: an app-owned bottom sheet or modal. The SDK will call it after a successful visible login when consent is unset. You call back:onAccept→ SDK stores credentials and writes provider consent.onSkip→ SDK does not store; consent remains unset (prompt again next time).onDeny→ SDK writes provider consent = 'denied' (no further prompts). Keys used internally (no need to manage these directly):
- Credentials:
zkp2p_cred_{keccak256(platform:actionType:url)} - Consent:
zkp2p_consent_{keccak256(platform:actionType:url)}
Provider config must include login selectors. Optionally set a reveal timeout for invisible autofill flows:
Exposing helpers (optional):
import { useZkp2p } from '@zkp2p/zkp2p-react-native-sdk';
import type { ProviderSettings } from '@zkp2p/zkp2p-react-native-sdk';
const ExampleComponent = ({ providerCfg }: { providerCfg: ProviderSettings }) => {
const {
clearAllCredentials,
clearAllConsents,
clearProviderCredentials,
clearProviderConsent,
getProviderConsent,
} = useZkp2p();
const handleClear = async () => {
await clearAllCredentials();
await clearAllConsents();
await clearProviderCredentials(providerCfg);
await clearProviderConsent(providerCfg);
const consent = await getProviderConsent(providerCfg);
console.log('Current consent', consent);
};
// ...
};
#### 5. `fulfillIntent(params)`
Complete the transaction by first posting your proof to the attestation service and then submitting the attestation on-chain.
Required params:
- `platform: string`, `actionType: string`
- `intentHash: Hash`
- `zkTlsProof: string` (stringified proof JSON)
- `amount: string`, `timestampMs: string`
- `fiatCurrency: Hex`, `conversionRate: string`
- `payeeDetails: Hex`, `timestampBufferMs: string`
- `verifyingContract?: Address` (UnifiedPaymentVerifier)
```typescript
const { zkp2pClient } = useZkp2p();
await zkp2pClient.fulfillIntent({
platform: 'revolut',
actionType: 'transfer_revolut',
intentHash: '0x…',
zkTlsProof: proofJsonString,
amount: '1000000',
timestampMs: String(Date.now()),
fiatCurrency: currencyInfo.USD.currencyCodeHash as Hex,
conversionRate: '1000000000000000000',
payeeDetails: '0x…' as Hex,
timestampBufferMs: '10000000',
verifyingContract: '0x…',
});Notes:
- SDK encodes
paymentProoffrom the serviceresponseObjectand setsverificationData = abi.encode(['address'], [signer]). - No metadata is used from the service for on-chain verification.
Use these as the “after” source of truth when upgrading.
// FulfillIntentParams
type FulfillIntentParams = {
intentHash: Hash;
zkTlsProof: string; // stringified proof JSON
platform: string;
actionType: string;
amount: string;
timestampMs: string;
fiatCurrency: Hex; // bytes32
conversionRate: string; // 1e18 scaled
payeeDetails: Hex; // bytes32
timestampBufferMs: string;
verifyingContract?: Address; // UnifiedPaymentVerifier
onSuccess?: ActionCallback;
onError?: (error: Error) => void;
onMined?: ActionCallback;
txOverrides?: SafeTxOverrides;
};
// SignalIntentParams
type SignalIntentParams = {
processorName: string;
depositId: string;
amount: string; // replaces tokenAmount
payeeDetails: string;
toAddress: Address; // was string
paymentMethodHash?: Hex; // optional override
currencyHash?: Hex; // replaces currency by code
conversionRate: string | bigint;
referrer?: Address;
referrerFee?: string | bigint;
onSuccess?: ActionCallback;
onError?: (error: Error) => void;
onMined?: ActionCallback;
txOverrides?: SafeTxOverrides;
};
// IntentSignalRequest (SDK → API)
type IntentSignalRequest = {
processorName: string;
payeeDetails: string;
depositId: string;
amount: string;
toAddress: Address;
paymentMethod: Hex;
fiatCurrency: Hex;
conversionRate: string;
chainId: string;
orchestratorAddress: Address;
escrowAddress: Address;
};
// SignalIntentResponse.responseObject.intentData
type IntentData = {
orchestratorAddress: Address;
escrowAddress: Address;
depositId: string;
amount: string;
recipientAddress: Address;
paymentMethod: Hex;
fiatCurrency: Hex;
conversionRate: string;
signatureExpiration: string;
chainId: string;
gatingServiceSignature: Hex;
};
// CreateDeposit
type CreateDepositConversionRate = {
currency: CurrencyType; // e.g., Currency.USD
conversionRate: string;
};
type CreateDepositParams = {
token: Address;
amount: bigint;
intentAmountRange: { min: bigint; max: bigint };
conversionRates: CreateDepositConversionRate[][]; // per payment method
processorNames: string[];
depositData: Record<string, string>[];
// new optionals
delegate?: Address;
intentGuardian?: Address;
referrer?: Address;
referrerFee?: string | bigint;
onSuccess?: ActionCallback;
onError?: (error: Error) => void;
onMined?: ActionCallback;
txOverrides?: SafeTxOverrides;
};
// Auto-generate proof callback
type AutoGenerateProofOptions = {
intentHash?: string;
itemIndex?: number;
onProofGenerated?: (proofData: ProofData | ProofData[]) => void;
onProofError?: (error: Error) => void;
};
// AttestationResponse (service output used by encoders)
type AttestationResponse = {
success: boolean;
message: string;
responseObject: {
signature: Hex;
signer: Address;
typedDataSpec: {
primaryType: string;
types: Record<string, { type: string; name: string }[]>;
};
typedDataValue: { intentHash: Hex; releaseAmount: string; dataHash: Hex };
encodedPaymentDetails: Hex;
metadata: Hex;
};
statusCode: number;
};Base API URL (versionless)
- Set
baseApiUrlto the root (e.g.,https://api.zkp2p.xyz). Do not append/v1or/v2; the SDK appends versioned paths internally. - Signal intent →
/v2/verify/intent; quotes →/v1/quote/(exact-fiat|exact-token); makers →/v1/makers/create.
const {
// State
flowState, // Current flow state: 'idle' | 'authenticating' | 'authenticated' | 'actionStarted' | 'proofGenerating' | 'proofGeneratedSuccess' | 'proofGeneratedFailure'
provider, // Current provider configuration
proofData, // Generated proof data (ProofData[])
metadataList, // List of transactions from authentication
authError, // Authentication error if any
proofError, // Proof generation error if any
interceptedPayload, // Network event data from authentication
authWebViewProps, // Props for the authentication WebView
// Methods
initiate, // Start the flow
authenticate, // Manual authentication
generateProof, // Generate proof(s) for transaction (returns ProofData[])
closeAuthWebView, // Close authentication modal
clearSession, // Clear cookies/storage for a fresh login
resetState, // Reset in-memory SDK state and cancel background work
// Client
zkp2pClient, // Direct access to contract methods (null in proof-only mode)
} = useZkp2p();- Circuits are now loaded lazily per algorithm (e.g.,
aes-256-ctr,aes-128-ctr,chacha20) instead of at SDK mount. This significantly reduces app startup time. - The SDK extracts the cipher from the witness and begins preloading that specific circuit just before proof generation. Only one circuit is initialized at a time, on demand.
- If you want to explicitly warm up a circuit even earlier, you may call the native preload via
GnarkBridge.preloadAlgorithm(algorithm)when you have enough context, though this is optional — the bridge also ensures lazy initialization on first use.
function BuyCrypto() {
const {
flowState,
initiate,
generateProof,
zkp2pClient,
proofData,
provider,
interceptedPayload
} = useZkp2p();
const handleBuy = async () => {
try {
const signalTx = await zkp2pClient.signalIntent({
processorName: 'venmo',
depositId: '123',
tokenAmount: '100',
payeeDetails: '0x1234567890123456789012345678901234567890',
toAddress: '0x0000000000000000000000000000000000000000',
currency: 'USD',
});
const intentHash = signalTx.responseObject.signedIntent;
await initiate('venmo', 'transfer_venmo', {
initialAction: {
enabled: true,
paymentDetails: {
venmoUsername: 'crypto-seller',
note: 'Cash',
amount: '100.00'
}
},
// Optional: After authenticate step, you can auto-generate proof
// (Recommended to pass autoGenerateProof in authenticate instead.)
});
// Example: Manually authenticate and auto-generate proof
await authenticate('venmo', 'transfer_venmo', {
autoGenerateProof: {
intentHash,
itemIndex: 0,
onProofGenerated: async (_singleProof) => {
// For multiple-proof configs, read the array from the hook state
const proofsToUse = proofData && proofData.length > 0 ? proofData : [];
if (proofsToUse.length === 0) return;
const fulfillTx = await zkp2pClient.fulfillIntent({
paymentProofs: proofsToUse,
intentHash,
onSuccess: (tx) => console.log('Transaction complete:', tx.hash),
onError: (error) => console.error('Fulfillment failed:', error),
});
console.log('Transaction complete:', fulfillTx.hash);
},
onProofError: async (error) => {
console.error('Auto-proof failed, trying manual:', error);
if (provider && interceptedPayload) {
const proofs = await generateProof(
provider,
interceptedPayload,
intentHash,
0
);
await zkp2pClient.fulfillIntent({ paymentProofs: proofs, intentHash });
}
},
},
});
} catch (error) {
console.error('Buy flow failed:', error);
}
};
return (
<View>
<Button onPress={handleBuy} title="Buy Crypto" />
<Text>Status: {flowState}</Text>
</View>
);
}idle- No active operationactionStarted- Payment action initiated (Venmo/CashApp/etc opened)authenticating- User authenticating with payment providerauthenticated- Authentication complete, transactions availableproofGenerating- Generating zero-knowledge proofproofGeneratedSuccess- Proof successfully generatedproofGeneratedFailure- Proof generation failed
The provider renders a default progress sheet. To supply your own UI, opt out and read the live status from context:
function App() {
return (
<Zkp2pProvider hideDefaultProofUI>
<HeadlessProofScreen />
</Zkp2pProvider>
);
}
function HeadlessProofScreen() {
const { proofStatus, cancelProof } = useZkp2p();
if (proofStatus.phase === 'idle') return null;
return (
<YourCustomIndicator
progress={proofStatus.progress}
message={proofStatus.meta}
status={proofStatus.phase}
error={proofStatus.error}
onCancel={cancelProof}
/>
);
}proofStatus updates as the proof lifecycle progresses:
phase:'idle' | 'running' | 'success' | 'failure'progress: normalized value from0to1meta: human-readable status messageerror: latest proof error (if any)
Call cancelProof() to stop the active proof and close the default sheet. Re-running generateProof(...) after a failure automatically reinitializes the RPC bridge. For a full reset, call resetState().
| Platform | Action Types | Description |
|---|---|---|
| Venmo | transfer_venmo |
Venmo P2P transfers |
| Cash App | transfer_cashapp |
Cash App transfers |
| Revolut | transfer_revolut |
Revolut transfers |
| Wise | transfer_wise |
Wise transfers |
| MercadoPago | transfer_mercadopago |
MercadoPago transfers |
| Zelle | transfer_zelle |
Zelle transfers |
// Check authentication errors
const { authError } = useZkp2p();
if (authError) {
console.error('Auth failed:', authError.message);
}
// Handle proof generation errors
try {
await generateProof(provider, interceptedPayload, intentHash, 0);
} catch (error) {
console.error('Proof generation failed:', error);
}The SDK allows configuring custom user agents per provider in the provider configuration:
provider.mobile?.userAgent = {
ios: 'Custom iOS User Agent',
android: 'Custom Android User Agent'
};The SDK supports generating multiple proofs for different transaction data:
const proofs = await generateProof(
provider,
interceptedPayload,
intentHash,
0 // itemIndex
);
// Returns ProofData[] - array of proofs if multiple configuredThe SDK automatically adjusts proof generation concurrency based on device memory:
- Devices with 8GB+ RAM: Up to 6 concurrent proofs
- Devices with 6GB+ RAM: Up to 4 concurrent proofs
- Devices with 4GB+ RAM: Up to 3 concurrent proofs
- Devices with less than 4GB: Up to 2 concurrent proofs
-
resetState()— Resets internal SDK state and cancels background proof tasks:- Cancels all active native gnark proofs and cleans up memory
- Aborts pending RPC requests and remounts the RPC bridge
- Clears
proofData,metadataList,interceptedPayload - Closes/minimizes auth webview and resets flow to
idle
-
clearSession(options?)— Clears persisted cookies/storage used for web auth. Use this to force fresh logins. It does not cancel native tasks by itself.
The SDK provides a built-in WebView component for authentication:
- Slide-up animation from bottom
- Minimizable to 48px height
- Tap header to minimize/expand
- Clean circular close button
- No backdrop overlay - allows interaction with main app
const { zkp2pClient } = useZkp2p();
// Available methods:
zkp2pClient.signalIntent(params) // Signal buy/sell intent
zkp2pClient.fulfillIntent(params) // Complete transaction with proof
zkp2pClient.createDeposit(params) // Create new deposit
zkp2pClient.withdrawDeposit(params) // Withdraw deposit
zkp2pClient.cancelIntent(params) // Cancel pending intent
zkp2pClient.releaseFundsToPayer(params) // Release escrowed funds
// Query methods:
zkp2pClient.getQuote(params) // Get price quotes
zkp2pClient.getPayeeDetails(params) // Get payee information
zkp2pClient.getAccountDeposits(address) // Get user's deposits
zkp2pClient.getAccountIntents(address) // Get user's intents (array)
// Utility methods:
zkp2pClient.getUsdcAddress() // Get USDC contract address
zkp2pClient.getDeployedAddresses() // Get all contract addressesRequired
paymentPlatforms: string[]fiatCurrency: stringuser: string— taker addressrecipient: string— on-chain recipientdestinationChainId: numberdestinationToken: stringamount: string— useisExactFiatto indicate fiat vs token units
Optional
isExactFiat?: boolean— defaults to truequotesToReturn?: number— API limits server-sidereferrer?: string,useMultihop?: booleanescrowAddresses?: string[]— override the escrow(s) used for quoting; defaults to[zkp2pClient.getDeployedAddresses().escrow]minDepositSuccessRateBps?: number— minimum acceptable historical completion rate; defaults to3000(30%)
getQuote(params) returns { success, message, responseObject, statusCode } where responseObject contains:
fiat:{ currencyCode, currencyName, currencySymbol, countryCode }token:{ token, decimals, name, symbol, chainId }quotes: array of quotes with fields:fiatAmount,fiatAmountFormattedtokenAmount,tokenAmountFormattedpaymentMethod,payeeAddress,conversionRateintent:{ depositId, amount, processorName, toAddress, payeeDetails, fiatCurrencyCode, chainId, escrowAddress? }payeeData?: populated by the SDK whenapiKeyis provideddepositSuccessRateBps?: historical completion rate for the source deposit, in basis points (0–10000)depositIntentStats?:{ totalIntents, signaledIntents, fulfilledIntents, prunedIntents }
Example quote item
{
"fiatAmount": "1000000",
"fiatAmountFormatted": "1.00 USD",
"tokenAmount": "990099",
"tokenAmountFormatted": "0.99 USDC",
"paymentMethod": "venmo",
"payeeAddress": "0x...",
"conversionRate": "1010000000000000000",
"depositSuccessRateBps": 4166,
"depositIntentStats": {
"totalIntents": 12,
"signaledIntents": 12,
"fulfilledIntents": 5,
"prunedIntents": 7
},
"intent": {
"depositId": "1910",
"amount": "985221",
"processorName": "venmo",
"toAddress": "0x...",
"payeeDetails": "0x...",
"fiatCurrencyCode": "0x...",
"chainId": "8453",
"escrowAddress": "0x..."
}
}- When calling
getAccountDeposits(address)andgetAccountIntents(address), the SDK parses on-chain views. - Enrichment adds two fields:
paymentMethod: platform key derived from the verifier address (e.g.,venmo,cashapp,revolut,wise). Always set when the verifier is a supported platform.paymentData: opaque key-value details fetched from the API, available only when anapiKeyis provided.
Where the data appears
- Per-verifier:
EscrowDepositView.verifiers[i].verificationData.paymentMethodand...verificationData.paymentData. - Top-level intent:
EscrowIntent.paymentMethodandEscrowIntent.paymentData(copied from the verifier matchingpaymentVerifier).
Example
const intentViews = await zkp2pClient.getAccountIntents('0xYourAddress');
if (intentView) {
// Top-level enrichment
console.log(intentView.intent.paymentMethod); // e.g. 'venmo'
console.log(intentView.intent.paymentData); // e.g. { username: 'alice', contact: '...' }
// Per-verifier enrichment
for (const v of intentView.deposit.verifiers) {
console.log(v.verificationData.paymentMethod);
console.log(v.verificationData.paymentData);
}
}
const deposits = await zkp2pClient.getAccountDeposits('0xYourAddress');
for (const d of deposits) {
for (const v of d.verifiers) {
console.log(v.verificationData.paymentMethod);
console.log(v.verificationData.paymentData);
}
}Notes
- Enrichment is best-effort; failures are logged and do not throw.
paymentDatarequires a validapiKey.paymentMethoddoes not.
- Supported payment platforms are defined once in
ENABLED_PLATFORMS(src/utils/constants.ts). - Contract addresses use a typed
ContractSetthat maps those platforms to verifier addresses, keeping config and types in sync. - Helpers:
platformFromVerifierAddress(addresses, verifierAddress)resolves the platform key from a verifier contract address.getPlatformAddressMap(addresses)returns{ [platform]: address }restricted to enabled platforms.
// Proof data structure
interface ProofData {
proofType: 'reclaim';
proof: ReclaimProof;
}
// Flow states
type FlowState =
| 'idle'
| 'authenticating'
| 'authenticated'
| 'actionStarted'
| 'proofGenerating'
| 'proofGeneratedSuccess'
| 'proofGeneratedFailure';
// Initiate options
interface InitiateOptions {
authOverrides?: AuthWVOverrides;
existingProviderConfig?: ProviderSettings;
initialAction?: {
enabled?: boolean;
paymentDetails?: Record<string, string>; // For URL/JS injection
useExternalActionOverride?: boolean; // Override internal vs external action
};
}
// Authenticate options
interface AuthenticateOptions {
authOverrides?: AuthWVOverrides;
existingProviderConfig?: ProviderSettings;
autoGenerateProof?: {
intentHash?: string;
itemIndex?: number;
onProofGenerated?: (proofData: ProofData) => void;
onProofError?: (error: Error) => void;
};
}
// Fulfill intent params (subset)
interface FulfillIntentParams {
paymentProofs: ProofData[];
intentHash: string;
}See the contributing guide to learn how to contribute to the repository and the development workflow.
MIT
Made with create-react-native-library
{ "mobile": { "login": { "usernameSelector": "#email, input[name=\"email\"]", "passwordSelector": "#password, input[name=\"password\"][type=\"password\"]", "submitSelector": "button[type=\"submit\"]", "revealTimeoutMs": 3000 // how long to keep the WebView minimized after submit before revealing } } }