@tagadapay/plugin-sdk
v3.0.15
Published
Modern React SDK for building Tagada Pay plugins
Readme
TagadaPay Plugin SDK
A comprehensive React SDK for building plugins on the TagadaPay platform. Create custom checkout experiences, landing pages, and interactive components with automatic configuration injection and advanced routing capabilities.
🚀 V2 Now Available! The new V2 architecture features TanStack Query integration, improved TypeScript support, and better performance. See V2 Architecture for details.
Recommended: Use
@tagadapay/plugin-sdk/v3for new projects. V1 (/react) is still supported for existing projects.
📚 Documentation
V2 Core APIs (Recommended)
- V2 Architecture - New TanStack Query-based architecture
- V2 Examples - Complete examples using the new V2 hooks
- useCheckout (V2) - TanStack Query-based checkout management
- useOffers (V2) - Dynamic pricing with automatic caching
- useProducts (V2) - Product data management
- useStoreConfig (V2) - Store configuration with automatic caching
- usePayment (V2) - Payment processing with 3DS support
- V2 Utility Functions - Enhanced utilities and TanStack Query integration
Legacy V1 APIs (Still Supported)
- useCheckout (V1) - Legacy checkout state management
- setCheckoutInfo - Customer information and validation
- useOffers (V1) - Legacy dynamic pricing
- usePromotionCodes - Legacy promotion code management
- Money utilities - Currency formatting and calculations
- URL utilities - Navigation and routing helpers
Plugin Development
- Plugin Configuration - How to access store context, config, and branding
- Initialization Modes - Choose between blocking and non-blocking initialization
- Google Autocomplete - Address autocomplete with Google Places API
- ISO Data - Country and region data with Google integration
Examples
- Vite Checkout Demo - Complete checkout implementation
🏗️ Building a Plugin
Plugin Structure
Every TagadaPay plugin follows this simple structure:
my-plugin/
├── plugin.manifest.json # Plugin metadata & routing
├── .local.json # Local dev config (auto-injected in production)
├── config/ # Optional deployment configs
│ ├── theme-green.json # Config variant A
│ └── theme-blue.json # Config variant B
├── src/
│ └── App.tsx # Your plugin code
└── dist/ # Built plugin filesConfiguration Flow
graph TD
A[🛠️ Local Development] --> B[.local.json]
A --> C[config/*.json]
B --> D[usePluginConfig()]
C --> D
E[🚀 Production] --> F[Platform Injection]
F --> G[HTTP Headers & Meta Tags]
G --> D
D --> H[Your Plugin Component]
style A fill:#e1f5fe
style E fill:#f3e5f5
style D fill:#fff3e0
style H fill:#e8f5e8Essential Files
1. plugin.manifest.json - Plugin Metadata
{
"pluginId": "my-awesome-plugin",
"name": "My Awesome Plugin",
"version": "1.0.0",
"mode": "direct-mode",
"router": {
"basePath": "/",
"matcher": ".*",
"excluder": "/checkout"
}
}2. .local.json - Local Development Context
{
"storeId": "store_abc123",
"accountId": "acc_xyz789",
"basePath": "/"
}⚠️ Auto-managed: This file is only for local dev. In production, the platform injects this data automatically.
3. config/my-theme.json - Optional Deployment Config
{
"configName": "green-theme",
"branding": {
"primaryColor": "#059669",
"companyName": "My Store",
"logoUrl": "https://example.com/logo.png"
},
"features": {
"enableChat": true,
"maxItems": 10
}
}📝 Note: Config can contain any keys you need - the SDK doesn't enforce a specific structure.
🚀 Quick Start
Installation
npm install @tagadapay/plugin-sdkBasic Plugin Setup
import React from 'react';
import {
TagadaProvider,
usePluginConfig,
useGoogleAutocomplete,
useISOData,
useCheckout,
formatMoney,
} from '@tagadapay/plugin-sdk/v3';
function MyPlugin() {
const { config, storeId, accountId, basePath, loading } = usePluginConfig();
// Optional: Add address autocomplete
const { searchPlaces, predictions } = useGoogleAutocomplete({
apiKey: config?.googleMapsApiKey || 'YOUR_API_KEY',
});
// Optional: Add country/region data
const { countries } = useISOData('en');
// V2: Use TanStack Query-based checkout hook
const { checkout, init, updateLineItems, isLoading } = useCheckout({
checkoutToken: 'your-checkout-token',
});
if (loading) return <div>Loading...</div>;
return (
<div style={{ '--primary': config?.branding?.primaryColor }}>
<h1>Welcome to {config?.branding?.companyName}</h1>
<p>Store: {storeId}</p>
<p>Base Path: {basePath}</p>
<p>Available Countries: {Object.keys(countries).length}</p>
{checkout && (
<div>
<h2>Checkout Total: {formatMoney(checkout.summary.total, checkout.currency)}</h2>
<p>Items: {checkout.lineItems.length}</p>
</div>
)}
</div>
);
}
// Wrap your plugin with TagadaProvider
function App() {
return (
<TagadaProvider>
<MyPlugin />
</TagadaProvider>
);
}
export default App;🏗️ V2 Architecture
What's New in V2
The TagadaPay Plugin SDK v3 introduces a clean architecture with significant improvements:
🔄 TanStack Query Integration
- Automatic Caching: All API calls are cached and synchronized across components
- Background Refetching: Data stays fresh with automatic background updates
- Optimistic Updates: Instant UI feedback with automatic rollback on errors
- Request Deduplication: Multiple components can use the same data without duplicate requests
🏗️ Clean Architecture
- Core Layer: Pure functions and API clients without React dependencies
- React Layer: Hooks and components that use core functions
- Better Testing: Easier to test business logic separately from UI logic
📦 Import Paths
// V2 (Recommended)
import { useCheckout, useOffers, TagadaProvider } from '@tagadapay/plugin-sdk/v3';
// Legacy (Still supported)
import { useCheckout, useOffers, TagadaProvider } from '@tagadapay/plugin-sdk/react';🔧 Enhanced Developer Experience
- TypeScript First: Better type inference and autocomplete
- Debug Tools: Built-in debug drawer for development
- Performance: Reduced bundle size and better performance
Migration from V1
Most hooks have the same names but improved APIs:
// V1 Pattern
const { checkout, loading, error } = useCheckout();
// V2 Pattern (TanStack Query)
const { checkout, isLoading, error, refresh } = useCheckout({
checkoutToken: 'token',
enabled: true,
});📚 V2 Examples
Complete Checkout Flow
import React, { useState } from 'react';
import {
TagadaProvider,
useCheckout,
useProducts,
usePluginConfig,
formatMoney,
} from '@tagadapay/plugin-sdk/v3';
function CheckoutPage() {
const [checkoutToken, setCheckoutToken] = useState<string>();
const { config } = usePluginConfig();
// Load products
const { products, isLoading: productsLoading } = useProducts({
storeId: config.storeId,
});
// Initialize checkout when needed
const {
checkout,
isLoading: checkoutLoading,
init,
updateLineItems,
updateCustomer,
applyPromotionCode,
} = useCheckout({
checkoutToken,
enabled: !!checkoutToken,
});
const handleInitCheckout = async () => {
if (!products?.length) return;
const result = await init({
lineItems: [
{
variantId: products[0].variants[0].id,
quantity: 1,
},
],
});
setCheckoutToken(result.checkoutToken);
};
const handleApplyPromo = async (code: string) => {
try {
await applyPromotionCode(code);
// TanStack Query automatically refetches and updates the UI
} catch (error) {
console.error('Failed to apply promo:', error);
}
};
if (productsLoading) return <div>Loading products...</div>;
return (
<div>
<h1>Checkout</h1>
{!checkoutToken ? (
<button onClick={handleInitCheckout}>Start Checkout</button>
) : (
<div>
{checkoutLoading ? (
<div>Loading checkout...</div>
) : checkout ? (
<div>
<h2>Order Summary</h2>
<p>Total: {formatMoney(checkout.summary.total, checkout.currency)}</p>
<p>Items: {checkout.lineItems.length}</p>
<div>
<input
type="text"
placeholder="Promo code"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleApplyPromo(e.currentTarget.value);
}
}}
/>
</div>
</div>
) : null}
</div>
)}
</div>
);
}
function App() {
return (
<TagadaProvider>
<CheckoutPage />
</TagadaProvider>
);
}Multi-Component Data Sharing
import React from 'react';
import { TagadaProvider, useCheckout, useOffers, formatMoney } from '@tagadapay/plugin-sdk/v3';
// Component 1: Cart Summary
function CartSummary({ checkoutToken }: { checkoutToken: string }) {
const { checkout, isLoading } = useCheckout({ checkoutToken });
if (isLoading) return <div>Loading...</div>;
if (!checkout) return null;
return (
<div>
<h3>Cart ({checkout.lineItems.length} items)</h3>
<p>Total: {formatMoney(checkout.summary.total, checkout.currency)}</p>
</div>
);
}
// Component 2: Available Offers (shares same checkout data automatically)
function OffersPanel({ checkoutToken }: { checkoutToken: string }) {
const { checkout } = useCheckout({ checkoutToken }); // Same data, no extra request!
const { offers, isLoading } = useOffers({
storeId: checkout?.storeId,
});
if (isLoading) return <div>Loading offers...</div>;
return (
<div>
<h3>Special Offers</h3>
{offers?.map((offer) => (
<div key={offer.id}>
<p>{offer.title}</p>
<p>Save {formatMoney(offer.discount, checkout?.currency || 'USD')}</p>
</div>
))}
</div>
);
}
// Main component
function CheckoutWithOffers() {
const checkoutToken = 'your-checkout-token';
return (
<div style={{ display: 'flex', gap: '20px' }}>
<CartSummary checkoutToken={checkoutToken} />
<OffersPanel checkoutToken={checkoutToken} />
</div>
);
}Optimistic Updates
import React from 'react';
import { useCheckout } from '@tagadapay/plugin-sdk/v3';
function QuantitySelector({
checkoutToken,
variantId,
currentQuantity,
}: {
checkoutToken: string;
variantId: string;
currentQuantity: number;
}) {
const { checkout, setItemQuantity, updateLineItemsOptimistic } = useCheckout({ checkoutToken });
const handleQuantityChange = async (newQuantity: number) => {
// 1. Optimistic update (instant UI feedback)
const updatedItems =
checkout?.lineItems.map((item) =>
item.variantId === variantId ? { ...item, quantity: newQuantity } : item,
) || [];
updateLineItemsOptimistic(updatedItems);
// 2. Server update (with automatic rollback on error)
try {
await setItemQuantity(variantId, newQuantity);
// TanStack Query automatically syncs the real data
} catch (error) {
// Automatic rollback to previous state
console.error('Failed to update quantity:', error);
}
};
return (
<div>
<button onClick={() => handleQuantityChange(currentQuantity - 1)}>-</button>
<span>{currentQuantity}</span>
<button onClick={() => handleQuantityChange(currentQuantity + 1)}>+</button>
</div>
);
}🚀 Initialization Modes
The TagadaProvider supports two initialization modes to give you control over when your components render:
Non-Blocking Mode (Default - Recommended)
<TagadaProvider>
{/* Children render immediately after config loads */}
<YourApp />
</TagadaProvider>
// OR explicitly
<TagadaProvider blockUntilSessionReady={false}>
<YourApp />
</TagadaProvider>Flow:
- Phase 1 & 2 ✅ Plugin config loads → Children render immediately
- Phase 3 🔄 Session initialization runs in background
- API calls 🔄 Hooks automatically wait for session to be ready
Benefits:
- ⚡ Faster rendering - UI appears immediately
- 🎯 Better UX - Show loading states while session initializes
- 🔄 Automatic waiting - Hooks handle session timing for you
Blocking Mode (Legacy Behavior)
<TagadaProvider blockUntilSessionReady={true}>
{/* Children render only after ALL initialization completes */}
<YourApp />
</TagadaProvider>Flow:
- All Phases ⏳ Config + Session must complete before children render
- API calls ✅ Work immediately (no waiting needed)
Use when:
- 🔄 Migrating from older SDK versions
- 🎯 Simple apps that don't need progressive loading
Console Logs
The SDK logs help you understand which mode you're using:
Non-blocking mode:
✅ Phase 1 & 2 Complete - Plugin config loaded
🚀 Non-blocking mode: Children can now render - Phase 3 will continue in background
🔄 [useCheckout] Waiting for session initialization to complete...
✅ Phase 3 Complete - Session initialization completed successfully
✅ [useCheckout] Session initialized, proceeding with checkout initBlocking mode:
✅ Phase 1 & 2 Complete - Plugin config loaded
⏳ Blocking mode: Children will render after Phase 3 completes
✅ Phase 3 Complete - Session initialization completed successfullyTagadaProvider API
interface TagadaProviderProps {
children: ReactNode;
environment?: 'local' | 'development' | 'staging' | 'production';
customApiConfig?: Partial<EnvironmentConfig>;
debugMode?: boolean;
localConfig?: string; // LOCAL DEV ONLY: Override config variant
blockUntilSessionReady?: boolean; // Default: false
// V2 Specific Options
queryClientConfig?: QueryClientConfig; // TanStack Query configuration
enableDevtools?: boolean; // Enable React Query Devtools
}| Prop | Type | Default | Description |
| ------------------------ | ----------- | -------------------- | ------------------------------ |
| children | ReactNode | - | Your plugin components |
| environment | string | auto-detect | Override environment detection |
| customApiConfig | object | - | Custom API configuration |
| debugMode | boolean | auto (false in prod) | Enable debug features & drawer |
| localConfig | string | 'default' | Config variant for local dev |
| blockUntilSessionReady | boolean | false | Use legacy blocking behavior |
| queryClientConfig | object | - | V2: TanStack Query config |
| enableDevtools | boolean | false | V2: React Query Devtools |
V2 Enhanced Provider Features
import { TagadaProvider } from '@tagadapay/plugin-sdk/v3';
function App() {
return (
<TagadaProvider
debugMode={true}
enableDevtools={true} // Shows React Query Devtools
queryClientConfig={{
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
},
},
}}
>
<YourApp />
</TagadaProvider>
);
}Version Compatibility: V2 features require
@tagadapay/plugin-sdk/v3. TheblockUntilSessionReadyoption was added in v2.3.0. For older versions, the blocking behavior was the default and only option.
Development vs Production
| Environment | Store/Account ID | Deployment Config | How it Works |
| -------------- | ---------------- | ----------------- | ------------------ |
| Local Dev | .local.json | config/*.json | Files on disk |
| Production | HTTP Headers | Meta Tags | Platform injection |
// ✅ ALWAYS use hooks - works in both environments
const { storeId, accountId, basePath, config } = usePluginConfig();
// ❌ NEVER access directly
// const config = window.__PLUGIN_CONFIG__; // Doesn't exist!🎯 Key Features
🔧 Plugin Configuration System
- Automatic Context Injection - Store ID, Account ID, and custom config
- Development & Production - Seamless environment switching
- Generic Configuration - Support for any JSON structure
- React Hooks - Clean, type-safe configuration access
💳 Payment Processing
- Secure tokenized payments
- Multiple payment method support
- Real-time validation
- PCI compliance
🌍 Address & Location
- Google Places Autocomplete - Automatic API loading and address parsing
- ISO Country/Region Data - Complete ISO 3166-1/3166-2 database
- Address Validation - Structured component extraction
- Multi-language Support - 11+ languages for international users
🛒 E-commerce Features
- Dynamic pricing and promotional offers
- Cart management and tax calculations
- Currency conversion and formatting
- Customer profile management
🎨 UI & Development
- Pre-built React components
- Customizable themes with configuration
- Mobile-optimized and accessible
- TypeScript support throughout
📖 API Reference
V2 Core Hooks (TanStack Query-based)
useCheckout()
Primary hook for checkout state management using TanStack Query for automatic caching and synchronization.
const {
// Query data
checkout, // CheckoutData | undefined
isLoading, // boolean
error, // Error | null
isSuccess, // boolean
// Actions
init, // (params: CheckoutInitParams) => Promise<{checkoutUrl, checkoutSession, checkoutToken}>
refresh, // () => Promise<void>
// Checkout operations
updateLineItems, // (lineItems: CheckoutLineItem[]) => Promise<any>
updateLineItemsOptimistic, // (lineItems: CheckoutLineItem[]) => void
setItemQuantity, // (variantId: string, quantity: number, priceId?: string) => Promise<any>
updateCustomer, // (data: {email: string, acceptsMarketing?: boolean}) => Promise<any>
applyPromotionCode, // (code: string) => Promise<any>
removePromotion, // (promotionId: string) => Promise<any>
} = useCheckout({ checkoutToken, enabled });useOffers()
Hook for managing dynamic offers and pricing with automatic cache management.
const {
// Query data
offers, // Offer[] | undefined
isLoading, // boolean
error, // Error | null
// Actions
refresh, // () => Promise<void>
} = useOffers({ storeId, enabled });usePromotions()
Hook for managing promotion codes in checkout sessions with TanStack Query.
const {
// Query data
promotions, // Promotion[] | undefined
isLoading, // boolean
error, // Error | null
// Actions
refresh, // () => Promise<void>
} = usePromotions({ checkoutToken, enabled });useProducts()
Hook for product data management with caching.
const {
// Query data
products, // Product[] | undefined
isLoading, // boolean
error, // Error | null
// Actions
refresh, // () => Promise<void>
} = useProducts({ storeId, enabled });useStoreConfig()
Hook for fetching store configuration with automatic caching.
const {
// Query data
storeConfig, // StoreConfig | undefined
isLoading, // boolean
error, // Error | null
isSuccess, // boolean
// Actions
refetch, // () => Promise<void>
} = useStoreConfig({ storeId, enabled });Example:
import { useStoreConfig } from '@tagadapay/plugin-sdk/v3';
function StoreInfo() {
const { storeConfig, isLoading } = useStoreConfig();
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{storeConfig?.storeName}</h1>
<p>Currency: {storeConfig?.currency}</p>
</div>
);
}See useStoreConfig documentation for more details.
useOrder()
Hook for order management and tracking.
const {
// Query data
order, // Order | undefined
isLoading, // boolean
error, // Error | null
// Actions
refresh, // () => Promise<void>
} = useOrder({ orderId, enabled });usePostPurchases()
Hook for post-purchase offers management.
const {
// Query data
postPurchases, // PostPurchaseOffer[] | undefined
isLoading, // boolean
error, // Error | null
// Actions
refresh, // () => Promise<void>
} = usePostPurchases({ orderId, enabled });usePayment()
Hook for payment processing with 3DS support.
const {
// Payment methods
processPayment, // (options: PaymentOptions) => Promise<PaymentResponse>
processApplePayPayment, // (token: ApplePayToken) => Promise<PaymentResponse>
// State
isProcessing, // boolean
error, // Error | null
} = usePayment();V2 Utility Functions & Advanced Features
TanStack Query Integration
V2 provides direct access to TanStack Query features for advanced use cases:
import {
useApiQuery,
useApiMutation,
useInvalidateQuery,
usePreloadQuery,
queryKeys,
} from '@tagadapay/plugin-sdk/v3';
// Custom API queries
const { data, isLoading } = useApiQuery({
queryKey: ['custom', 'endpoint'],
queryFn: () => apiClient.get('/custom-endpoint'),
});
// Mutations with automatic cache updates
const mutation = useApiMutation({
mutationFn: (data) => apiClient.post('/update', data),
onSuccess: () => {
// Invalidate related queries
invalidateQuery(['checkout']);
},
});
// Preload data for better UX
const preloadCheckout = usePreloadQuery();
preloadCheckout({
queryKey: queryKeys.checkout(checkoutToken),
queryFn: () => checkoutResource.get(checkoutToken),
});Money Utilities
Enhanced money formatting with better TypeScript support:
import { formatMoney, convertCurrency, getCurrencyInfo } from '@tagadapay/plugin-sdk/v3';
// Format money with automatic currency detection
const formatted = formatMoney(2999, 'USD'); // "$29.99"
const simple = formatSimpleMoney(2999, 'EUR'); // "29.99"
const withoutSymbol = formatMoneyWithoutSymbol(2999, 'GBP'); // "29.99"
// Currency conversion (if rates available)
const converted = convertCurrency(2999, 'USD', 'EUR'); // 2699
// Get currency information
const currencyInfo = getCurrencyInfo('USD');
// { symbol: '$', code: 'USD', minorUnits: 2 }
// Convert between major and minor units
const major = minorUnitsToMajorUnits(2999); // 29.99
const minor = moneyStringOrNumberToMinorUnits('29.99'); // 2999Core Functions (Pure Functions)
V2 separates core business logic from React hooks:
import {
CheckoutResource,
ProductsResource,
PaymentsResource,
PluginConfigUtils,
} from '@tagadapay/plugin-sdk/v3';
// Use core functions directly (useful for server-side or non-React contexts)
const checkoutResource = new CheckoutResource(apiClient);
const checkout = await checkoutResource.get(checkoutToken);
// Plugin config utilities
const config = PluginConfigUtils.getPluginConfig(rawConfig, context);
const isValid = PluginConfigUtils.validateConfig(config);Debug Tools
V2 includes built-in debugging tools:
import { TagadaProvider } from '@tagadapay/plugin-sdk/v3';
function App() {
return (
<TagadaProvider
debugMode={true} // Shows debug drawer in development
environment="development"
>
<YourApp />
</TagadaProvider>
);
}🛠️ Development
Local Development
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run buildTesting
# Run tests
npm test
# Run tests with coverage
npm run test:coverageLinting
# Check code style
npm run lint
# Fix linting issues
npm run lint:fix📦 Build & Deploy
Building Your Plugin
# Build optimized bundle
npm run build
# Analyze bundle size
npm run analyzeDeployment
# Deploy using TagadaPay CLI
npx @tagadapay/plugin-cli deploy
# Deploy specific environment
npx @tagadapay/plugin-cli deploy --env production🔧 Configuration
Plugin Configuration
{
"name": "My Checkout Plugin",
"version": "1.0.0",
"description": "Custom checkout experience",
"main": "dist/index.js",
"tagadapay": {
"type": "checkout",
"mode": "direct",
"framework": "react"
}
}🔐 Security
Best Practices
- Never store sensitive payment data
- Always validate user inputs
- Use HTTPS in production
- Implement proper error handling
- Follow PCI DSS guidelines
Token Management
// ✅ Good: Use tokenized payments
const paymentToken = await tokenizePayment(cardData);
await processPayment({ token: paymentToken });
// ❌ Bad: Never store raw card data
// const cardNumber = '4111111111111111'; // Don't do this📱 Mobile Optimization
Responsive Design
.checkout-container {
max-width: 600px;
margin: 0 auto;
padding: 16px;
}
@media (max-width: 768px) {
.checkout-container {
padding: 8px;
}
.checkout-form {
font-size: 16px; /* Prevent zoom on iOS */
}
}Touch Interactions
const handleTouchStart = (e) => {
// Optimize for touch devices
e.preventDefault();
// Handle touch interaction
};🌐 Internationalization
Multi-language Support
import { useTranslation } from '@tagadapay/plugin-sdk';
function CheckoutForm() {
const { t } = useTranslation();
return (
<form>
<label>{t('checkout.email')}</label>
<input type="email" placeholder={t('checkout.email_placeholder')} />
</form>
);
}Currency Support
import { useCurrency } from '@tagadapay/plugin-sdk';
function PriceDisplay({ amount }) {
const { formatPrice, currency } = useCurrency();
return <span>{formatPrice(amount, currency)}</span>;
}📊 Analytics & Monitoring
Event Tracking
import { trackEvent } from '@tagadapay/plugin-sdk';
// Track user interactions
trackEvent('checkout_started', {
product_id: 'prod_123',
value: 2999,
currency: 'USD',
});
// Track conversions
trackEvent('purchase_completed', {
transaction_id: 'txn_456',
value: 2999,
currency: 'USD',
});Performance Monitoring
import { performance } from '@tagadapay/plugin-sdk';
// Measure load times
performance.mark('checkout-start');
// ... checkout logic
performance.mark('checkout-end');
performance.measure('checkout-duration', 'checkout-start', 'checkout-end');🤝 Contributing
Development Setup
# Clone the repository
git clone https://github.com/tagadapay/plugin-sdk.git
# Install dependencies
npm install
# Start development
npm run devSubmitting Changes
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
📄 License
MIT License - see LICENSE for details.
🆘 Support
- Documentation: docs.tagadapay.com
- Discord: discord.gg/tagadapay
- Email: [email protected]
- GitHub Issues: github.com/tagadapay/plugin-sdk/issues
Built with ❤️ by the TagadaPay team
