@yodlpay/yapp-sdk
v1.10.3
Published
SDK for building Yodl Yapps
Readme
🚀 Yodl Yapp SDK
The official SDK for building Yodl Yapps. This SDK provides a secure way to interact with the Yodl platform, handling authentication, payment requests, and cross-origin communication.
📦 Installation
npm install @yodlpay/yapp-sdk⚡ Quick Start
import YappSDK from '@yodlpay/yapp-sdk';
// Initialize the SDK with default configuration
const sdk = new YappSDK();💰 Payment Creation Example
Here's a focused example demonstrating how to create payments with the YappSDK:
import React, { useState, useEffect } from 'react';
import YappSDK, { FiatCurrency, Payment, isInIframe } from '@yodlpay/yapp-sdk';
// Initialize with minimal configuration
const sdk = new YappSDK();
function PaymentExample() {
const [paymentResult, setPaymentResult] = useState<Payment | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Check for payment information in URL on component mount
useEffect(() => {
// Parse payment information from URL (for redirect flow)
const urlPaymentResult = sdk.parsePaymentFromUrl();
if (urlPaymentResult) {
// Payment was successful via redirect
setPaymentResult(urlPaymentResult);
console.log('Payment successful (redirect):', urlPaymentResult);
// Clean the URL to prevent duplicate processing on refresh
// Note: You would need to implement this method or use history API
cleanPaymentUrl();
}
}, []);
// Helper function to clean payment parameters from URL
const cleanPaymentUrl = () => {
// Remove payment parameters from URL without page refresh
const url = new URL(window.location.href);
url.searchParams.delete('txHash');
url.searchParams.delete('chainId');
window.history.replaceState({}, document.title, url.toString());
};
// Create a new payment
const createPayment = async () => {
setIsLoading(true);
setError(null);
try {
// Recipient address - replace with your actual recipient
const recipientAddress = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e';
// Create a unique memo/order ID
const orderId = `order_${Date.now()}`;
// Request payment
const response = await sdk.requestPayment({
addressOrEns: recipientAddress,
amount: 50,
currency: FiatCurrency.USD,
memo: orderId,
redirectUrl: window.location.href, // Required for non-iframe mode
});
// Handle successful payment
setPaymentResult(response);
console.log('Payment successful:', response);
} catch (error) {
// Handle payment errors
console.error('Payment failed:', error);
if (error.message === 'Payment was cancelled') {
setError('Payment was cancelled by user');
} else if (error.message === 'Payment request timed out') {
setError('Payment request timed out after 5 minutes');
} else {
setError(`Payment failed: ${error.message}`);
}
} finally {
setIsLoading(false);
}
};
return (
<div className="payment-container">
<h1>Payment Example</h1>
{/* Payment result */}
{paymentResult && (
<div className="success-message">
<h2>Payment Successful!</h2>
<p>
<strong>Transaction Hash:</strong> {paymentResult.txHash}
</p>
<p>
<strong>Chain ID:</strong> {paymentResult.chainId}
</p>
</div>
)}
{/* Error message */}
{error && (
<div className="error-message">
<p>{error}</p>
</div>
)}
{/* Loading indicator */}
{isLoading && (
<div className="loading">
<p>Processing...</p>
</div>
)}
{/* Action buttons */}
<div className="button-group">
<button
onClick={createPayment}
disabled={isLoading}
className="payment-button"
>
Create New Payment
</button>
</div>
</div>
);
}
export default PaymentExample;Key Points About Payment Creation
Singleton SDK Instance
- The SDK is initialized once outside the component as a singleton
- This allows the same SDK instance to be reused across multiple components
Creating a Payment
- The
createPaymentfunction demonstrates how to request a new payment - It includes proper error handling for common scenarios (cancellation, timeout)
- A unique memo/order ID is generated for each payment
- The
Payment States
- The example tracks loading states, errors, and successful payments
- It provides appropriate UI feedback for each state
Redirect Flow Support
- The component checks for payment information in the URL on mount using
parsePaymentFromUrl() - Successfully handles both iframe and redirect payment flows
- Cleans up URL parameters after processing to prevent duplicate handling
- The component checks for payment information in the URL on mount using
🔄 Payment Flow
The SDK provides a streamlined payment flow:
// Make a payment request
const response = await sdk.requestPayment(address, config);The payment flow handles both iframe and redirect modes automatically based on the environment.
Payment Configuration
amount: Payment amount (positive number)currency: Currency code fromFiatCurrencyenummemo: Optional identifier/description (max 32 bytes)redirectUrl: Required when not in iframe mode
Payment Response Structure
The requestPayment method returns a Promise that resolves to a Payment object with the following structure:
interface Payment {
txHash: string; // Transaction hash (Hex string)
chainId: number; // Chain ID where transaction was executed
}This basic response provides essential information to track the payment on-chain. To get more detailed information, use the getPayment method. See Fetching Payment Details.
Payment Modes
The SDK operates in two different modes depending on the environment:
Iframe Mode
- Used automatically when your Yapp is running inside an iframe
- Communicates using the postMessage API
- Doesn't require a redirectUrl
- Provides a seamless in-app experience
Redirect Mode
- Used when your Yapp is running standalone (not in an iframe)
- Requires a
redirectUrlparameter to return after payment - Uses URL parameters and session storage to track payment state
- Handles browser tab visibility changes to detect returns from payment flow
Error Handling
The payment request might throw errors in several scenarios:
try {
const response = await sdk.requestPayment('0x123...', config);
// Handle successful payment
} catch (error) {
if (error.message === 'Payment was cancelled') {
// User cancelled the payment
} else if (error.message === 'Payment request timed out') {
// Payment took too long (default timeout: 5 minutes)
} else if (error.message.includes('ENS name not found')) {
// ENS name resolution failed
} else if (error.message.includes('Invalid currency')) {
// Unsupported currency was specified
} else if (error.message.includes('Memo exceeds maximum size')) {
// Memo was too large (max 32 bytes)
} else if (error.message.includes('Amount must be a positive number')) {
// Amount was invalid
} else if (error.message.includes('Redirect URL is required')) {
// Running outside iframe without redirectUrl
} else {
// Other errors
}
}Memo Field Usage
The memo field serves two purposes:
- 📝 Human-readable payment description
- 🔍 Unique identifier for payment tracking
Example use cases:
const examples = [
{ amount: 50, currency: FiatCurrency.USD, memo: 'subscription_id_456' },
{ amount: 75, currency: FiatCurrency.EUR, memo: 'invoice_789' },
{ amount: 120, currency: FiatCurrency.THB, memo: 'product_xyz_123' },
];🔍 Fetching Payment Details
Fetch payment details of a single or multiple payments from the Yodl indexer API.
Fetching a single payment
Fetch a single payment by passing the transaction hash to the getPayment method.
const payment = await sdk.getPayment(txHash);Fetching multiple payments
Fetch multiple payments by passing a config object with filtering options to the getPayments method.
const payments = await sdk.getPayments(config);Payment objects are of type PaymentSimple and has the following structure:
{
"payment": {
"chainId": 8453,
"txHash": "0x123c86bcf2a0aeadd269f30719a6ce7eef515a1a36600751a42ca77d42c802bc",
"paymentIndex": 0,
"destinationChainId": null,
"destinationTxHash": null,
"blockTimestamp": "2025-02-24T12:09:37.000Z",
"tokenOutSymbol": "USDC",
"tokenOutAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"tokenOutAmountGross": "10.03888",
"receiverAddress": "0xa1833B1A4DC461D3C025DbC99B71b127AEdbA45c",
"receiverEnsPrimaryName": null,
"invoiceCurrency": "USD",
"invoiceAmount": "10.04",
"senderAddress": "0x065c1BC23aE559BFFDBE5bbc335C30f30bE2b992",
"senderEnsPrimaryName": "maradona.yodl.eth"
}
}ENS Name Resolution
The SDK supports both standard Ethereum addresses and ENS (Ethereum Name Service) names as payment recipients:
// Using a standard Ethereum address
await sdk.requestPayment('0x742d35Cc6634C0532925a3b844Bc454e4438f44e', config);
// Using an ENS name
await sdk.requestPayment('vitalik.eth', config);When using ENS names, there are some important considerations:
Resolution Handling
- ENS resolution happens on the Yodl side
- If the ENS name cannot be resolved, the error
ENS name not found: [name]will be thrown - Always handle this error appropriately in your application
Performance Implications
- ENS resolution adds a small delay to the payment process
- For performance-sensitive applications, consider resolving ENS names in advance and using the resulting address
Testing
try { await sdk.requestPayment('nonexistent-name.eth', config); } catch (error) { if (error.message.includes('ENS name not found')) { // Handle ENS resolution failure console.error('The ENS name could not be resolved'); } }
Advanced Payment Tracking
For applications requiring robust payment tracking:
Generate Unique Memos
// Generate a unique memo combining order ID and timestamp const uniqueMemo = `order_${orderId}_${Date.now()}`;Store Payment State
// Store pending payment state in your database await storePaymentIntent({ orderId, memo: uniqueMemo, amount: 50, currency: FiatCurrency.USD, status: 'pending', timestamp: new Date(), });Update After Completion
// After payment completes await updatePaymentStatus({ memo: uniqueMemo, status: 'completed', txHash: payment.txHash, chainId: payment.chainId, });Handle Timeouts
The SDK automatically handles payment timeouts (default: 5 minutes), but your application should implement additional timeout handling for robustness.
// Set up payment timeout in your application setTimeout( async () => { const payment = await getPaymentByMemo(uniqueMemo); if (payment.status === 'pending') { await updatePaymentStatus({ memo: uniqueMemo, status: 'timeout', }); // Notify user or trigger recovery flow } }, 5 * 60 * 1000 + 30000, ); // 5 minutes + 30 seconds buffer
This detailed information can be used for:
- Verifying payment amounts and currencies
- Recording sender and receiver information
- Tracking payment timestamps
- Implementing receipt generation
🔐 Sign-In with Ethereum (SIWE)
The SDK provides a secure way to request SIWE (Sign-In with Ethereum) message signatures from users through the parent app:
try {
const response = await sdk.signSiweMessage({
domain: 'example.com',
uri: 'https://example.com/login',
version: '1',
chainId: 1,
nonce: 'random-nonce-123',
issuedAt: new Date().toISOString(),
statement: 'Sign in with Ethereum to the app.',
});
console.log('Signature:', response.signature);
console.log('Address:', response.address);
} catch (error) {
if (error.message === 'Signature request was cancelled') {
console.log('User cancelled the signing request');
} else if (error.message === 'Signature request timed out') {
console.log('Signature request timed out after 5 minutes');
} else {
console.error('Signing failed:', error.message);
}
}SIWE Request Data
The signSiweMessage method accepts a SiweRequestData object with the following structure:
interface SiweRequestData {
domain: string; // The domain requesting the signature
uri: string; // The URI where the signature will be used
version: string; // The SIWE version (typically '1')
chainId: number; // The chain ID where the signature will be valid
nonce: string; // A unique nonce to prevent replay attacks
issuedAt: string; // ISO timestamp of when the message was issued
statement?: string; // Optional statement to be signed
}SIWE Response Data
The method returns a Promise that resolves to a SiweResponseData object:
interface SiweResponseData {
signature: string; // The EIP-191 signature of the SIWE message
address: string; // The address that signed the message
}Error Handling
The signature request might throw errors in several scenarios:
Signature request was cancelled: User cancelled the signature requestSignature request timed out: Request took too long (default timeout: 5 minutes)SIWE signing is only supported in iframe mode: Attempted to use SIWE outside of iframe context
🖼️ Iframe Integration
Detection
import { isInIframe } from '@yodlpay/yapp-sdk';
if (isInIframe()) {
console.log('Running inside an iframe');
// Implement iframe-specific logic
} else {
console.log('Running in the main window');
// Implement redirect-based flow
}Closing the Yapp
try {
sdk.close('https://parent-origin.com');
} catch (error) {
console.error('Failed to close:', error);
}🔒 Security Best Practices
Origin Security
- Use HTTPS in production
- Validate message origins in iframe mode
- Set appropriate Content Security Policy (CSP) headers
Payment Handling
- Store
memovalues securely - Implement proper error handling
- Use timeouts appropriately (default: 5 minutes)
- Validate the returned payment parameters to prevent spoofing
- Store
Session Storage Considerations
- The SDK uses session storage to maintain payment state during redirects
- This data is automatically cleared after payment completion or timeout
- Consider implementing additional cleanup mechanisms for abandoned flows
- Sensitive payment data (like customer information) should not be stored in the memo field
Cross-Origin Communication
- Only accept messages from configured origins
- Validate all incoming messages
- Use secure postMessage communication
📚 API Reference
For detailed API documentation, please run:
npm run docsThis will generate comprehensive API documentation in the docs directory.
🤝 Contributing
We welcome contributions! Please see our contributing guidelines for more details.
📄 License
MIT License - see LICENSE file for details.
Development
Path Aliases
This codebase uses TypeScript path aliases for cleaner imports:
@utils- Points tosrc/utils@managers- Points tosrc/managers@constants- Points tosrc/constants@types- Points tosrc/types
These aliases are properly resolved during build time using tsc-alias and tsconfig-paths-webpack-plugin for the CommonJS and UMD outputs, respectively.
