💳 Parent Subscriptions
This document covers the comprehensive subscription system for parents in the Com DEALL mobile application, including platform-specific subscription management, payment processing, and subscription lifecycle management for both Android and iOS platforms.
🎯 Overview
The parent subscription system enables parents to purchase and manage subscriptions for their children across different platforms. The system provides platform-specific implementations for Android (using Razorpay) and iOS (using App Store), with comprehensive subscription lifecycle management, payment processing, and transaction tracking.
📱 Platform-Specific Implementation
The subscription system is implemented differently for Android and iOS platforms to comply with their respective payment ecosystems and store policies.
Android Subscription System
The Android implementation uses Razorpay for payment processing and subscription management, providing a seamless payment experience with comprehensive error handling and status tracking.
1. Android Subscription Flow
1. Subscription Plan Loading and Display
The Android subscription system loads available subscription plans from the server and displays them with comprehensive filtering and validation logic.
const [subscriptionType, setSubscriptionType] = useState<Subscription_Type_Enum>(
Subscription_Type_Enum.Monthly
);
const [selectedSubscriptionPlan, setSelectedSubscriptionPlan] = useState<TSubscriptionPlan | null>(null);
Purpose: Tracks selected subscription type (Monthly/Yearly) and chosen plan
typescriptconst { data, loading, error, refetch } = useGetSubscriptionPlansQuery({
fetchPolicy: 'cache-and-network',
});
Fetch Policy: cache-and-network - Shows cached data immediately, then updates from network
Purpose: Loads all available subscription plans for display
2. Parent's Children Query
const { data, loading, error } = useGetParentChildsTherapistsQuery({
variables: { parent_id: userData?.parentId },
fetchPolicy: 'network-only',
});
Fetch Policy: network-only - Always fetches fresh data Purpose: Gets list of parent's children to manage their subscriptions
2.Subscription Plan Filtering & Validation
- Current Plan Check
const checkIsCurrentPlan = (planId, subscriptionType, isFreePlan = false) => {
const subscription = childSubscriptionsData?.child_subscription.find(
sub => sub.subscription_status === Activity_Status_Enum_Enum.Active
);
const subscription_plan_id = subscription?.subscription_product?.subscription_plan_id;
return planId === subscription_plan_id;
};
- Purpose: Identifies if a plan is the user's currently active subscription
- Logic: Finds active subscription and compares plan IDs
- Special Case: Free plans ignore subscription type
- Upgrade Validation
const canUpgradePlan = (subscriptionPlan) => {
const currentPlan = childSubscriptionsData?.child_subscription.find(
sub => sub?.subscription_type === subscriptionType &&
sub.subscription_status === Activity_Status_Enum_Enum.Active
);
const currentPlanPrice = currentPlan?.subscription_product?.price;
const isCurrentPlan = checkIsCurrentPlan(planId, subscriptionType, isFreePlan);
Gets: Current active plan and its price
Upgrade Logic
typescriptif (!isFreePlan && !isCurrentPlan) {
if (subscriptionType === Subscription_Type_Enum.Monthly &&
monthlySubscription?.price > (currentPlanPrice ?? 0)) {
return true;
}
if (subscriptionType === Subscription_Type_Enum.Yearly &&
yearlySubscription?.price > (currentPlanPrice ?? 0)) {
return true;
}
}
return false;
Conditions for Upgrade:
Not a free plan Not the current plan New plan price > current plan price Matches selected subscription type (Monthly/Yearly)
Business Rules
Free plans cannot be upgraded to Current plan shows as "Active" (not upgradeable) Lower-priced plans are considered downgrades (not shown as upgrades) Price comparison is type-specific (Monthly vs Monthly, Yearly vs Yearly)
3.Razorpay Payment Integration
- Payment Status Management
const [paymentStatusModal, setPaymentStatusModal] = useState<{
isShow: boolean;
message?: string;
status?: PaymentStatus;
}>({
isShow: false,
message: '',
});
Purpose: Tracks payment result modal visibility, message, and status (Success/Failed)
- Razorpay Subscription Creation
const [createRazorpaySubscription, { loading }] = useCreateRazorpaySubscriptionMutation({
onCompleted(data) {
if (data?.createRazorpaySubscription?.success) {
const razorpay_subscription_id = data?.createRazorpaySubscription?.data?.razorpay_subscription_id;
const price = data?.createRazorpaySubscription?.data?.price;
if (razorpay_subscription_id && price) {
handleRazorPayPayment(razorpay_subscription_id, parseInt(price));
}
}
},
onError(error) {
toast.show({ text: error?.message });
},
});
Flow: Backend creates Razorpay subscription → Returns subscription ID & price → Triggers payment
- Razorpay Checkout Configuration
const options: CheckoutOptions = {
description: 'Subscription Payment',
currency: 'INR',
key: process.env.EXPO_PUBLIC_RAZOR_PAY_KEY!,
name: 'ComDEALL',
amount: amount,
prefill: {
email: parentInfo?.parent1_email,
contact: parentInfo?.parent1_phone,
name: parentInfo?.parent1_name,
},
theme: { color: '#EE7B36' },
timeout: 600,
modal: {
handleback: false, // Prevents back button
escape: false, // Prevents escape key
confirm_close: true, // Confirms before closing
},
};
Configuration: Pre-fills user details, sets timeout (10 mins), locks modal to prevent accidental closure
- Dynamic ID Assignment
if (canBuyPlan()) {
options['subscription_id'] = id; // For new subscription
} else {
options['order_id'] = id; // For one-time payment/upgrade
}
Logic: Uses subscription_id for recurring plans, order_id for one-time transactions
- Payment Processing
RazorpayCheckout.open(options)
.then((res: any) => {
const paymentConfirmationData = {
payment_signature: res.razorpay_signature,
provider_id: res.razorpay_payment_id,
};
if (canBuyPlan()) {
paymentConfirmationData['razorpay_subscription_id'] = res.razorpay_subscription_id;
} else {
paymentConfirmationData['razorpay_order_id'] = res.razorpay_order_id;
}
handleConfirmPayment(paymentConfirmationData, true);
})
.catch((err: any) => {
setPaymentStatusModal({
isShow: true,
message: err?.error?.description,
status: PaymentStatus.Failed,
});
});
- Success: Collects payment signature, payment ID, and subscription/order ID → Confirms payment
- Failure: Shows error modal with failure message
iOS Subscription System
The iOS implementation uses the App Store's in-app purchase system with RevenueCat integration for subscription management, providing native iOS subscription experience.
1. State Management
const [subscriptionStatus, setSubscriptionStatus] = useState<SubscriptionStatus>({
isActive: false,
});
const [hasCurrentActiveSubscriptions, setHasCurrentActiveSubscriptions] = useState(false);
Purpose: Tracks overall subscription status and whether user has any active subscriptions
2. IAP Hook Integration
const {
connected, // IAP connection status
subscriptions, // Available subscription products
fetchProducts, // Load products from App Store
getAvailablePurchases, // Get user's purchase history
hasActiveSubscriptions, // Check if any subscription is active
activeSubscriptions, // List of active subscriptions
getActiveSubscriptions, // Fetch active subscriptions
finishTransaction, // Complete purchase transaction
currentPurchase, // Latest purchase attempt
currentPurchaseError, // Purchase error if any
} = useIAP();
Purpose: React Native IAP library hook providing all App Store subscription management functions
3. Load Subscriptions from App Store
const loadSubscriptions = async () => {
try {
setLoading(true);
const skus = products?.map(p => p?.product_id!); // Extract product IDs
await fetchProducts({ skus: skus, type: 'subs' }); // Fetch from App Store
const hadActiveSubscriptions = await hasActiveSubscriptions();
setHasCurrentActiveSubscriptions(hadActiveSubscriptions);
} catch (error) {
console.error('Failed to load subscriptions:', error);
}
};
Flow:
- Get product IDs from database/backend
- Fetch subscription details from App Store using SKUs
- Check if user has any active subscriptions
- Update state with availability
4. Check Subscription Status
const checkSubscriptionStatus = async () => {
try {
const skus = products?.map(p => p?.product_id!);
const availablePurchases = await getAvailablePurchases(skus); // Get purchase history
getActiveSubscriptions(); // Refresh active subscriptions
const activeSubscription = findActiveSubscription(availablePurchases);
if (activeSubscription) {
const status = await validateSubscriptionStatus(activeSubscription);
setSubscriptionStatus(status);
} else {
setSubscriptionStatus({ isActive: false });
}
} catch (error) {
console.error('Failed to check subscription status:', error);
}
};
Flow:
- Get user's purchase history from App Store
- Find active subscription from purchases
- Validate subscription status (expiry, cancellation, etc.)
- Update subscription status state
2. Subscription Product Management**
The iOS system manages subscription products with comprehensive group management and product validation.
1. Product & Group Organization
const { products, groups } = useMemo(() => {
const products: TSubscriptionProduct[] = [];
const groups: TSubscriptionGroups = {};
subscriptionPlansData?.subscription_plans?.forEach(plan => {
if (plan?.subscription_plan_enum === Subscription_Plan_Enum_Enum?.Other) {
plan?.subscription_products?.forEach(product => {
if (
product?.platform === Subscription_Platform_Enum?.Ios &&
product?.product_id &&
product?.group_id
) {
products.push(product);
if (!groups[product?.group_id]) {
groups[product?.group_id] = [product];
} else {
groups[product?.group_id].push(product);
}
}
});
}
});
return { products, groups };
}, [subscriptionPlansData]);
Purpose: Filters and organizes iOS subscription products from backend data Logic:
- Only includes iOS platform products
- Excludes free plans (only "Other" plan type)
- Groups products by group_id (e.g., Monthly group, Yearly group)
- useMemo prevents recalculation on every render
Result:
- products = flat list of all iOS products
- groups = products organized by subscription group
2. Find Active Subscription
const findActiveSubscription = purchases => {
const skus = products?.map(p => p?.product_id!);
return purchases?.find(purchase => {
if (!skus?.includes(purchase.productId)) {
return false;
}
return isSubscriptionActive(purchase);
});
};
Purpose: Searches user's purchase history for an active subscription Logic:
- Get all valid product IDs (SKUs)
- Check if purchase matches a valid product
- Validate if subscription is still active
Returns: First active subscription or undefined
- Subscription Activity Validation
const isSubscriptionActive = purchase => {
const currentTime = Date.now();
if (Platform.OS === 'ios') {
if (purchase) {
return purchase.expirationDateIos > currentTime; // Check expiry date
}
// Sandbox environment handling (for testing)
if (purchase.environmentIOS === 'Sandbox') {
const dayInMs = 24 * 60 * 60 * 1000;
if (
purchase.transactionDate &&
currentTime - purchase.transactionDate < dayInMs
) {
return true; // Sandbox subscriptions valid for 1 day
}
}
}
return false;
};
Purpose: Determines if a subscription is currently active iOS Logic:
- Production: Checks if expirationDateIos is in the future
- Sandbox: Valid for 24 hours from purchase (for testing purposes)
Returns: true if subscription is active, false otherwise
3. iOS Purchase Processing**
The iOS system handles App Store purchases with comprehensive error handling and transaction management.
1. IAP Hook Setup
const { connected, requestPurchase } = useIAP();
connected: Boolean indicating if App Store connection is active requestPurchase: Function to initiate subscription purchase
2. Purchase Subscription Function
const purchaseSubscription = async (productId: string) => {
if (!connected) {
toast.show({ text: 'Store is not connected' });
return;
}
try {
setLoading(true);
await requestPurchase({
request: {
ios: {
sku: productId,
andDangerouslyFinishTransactionAutomatically: false,
appAccountToken: selectedChild?.id,
},
},
type: 'subs',
});
} catch (error) {
toast.show({ text: 'Failed to start subscription purchase' });
} finally {
setLoading(false);
}
};
Key Parameters:
-
sku: Product ID from App Store Connect
-
Dangerously Finish Transaction Automatically: false
-
Critical: Manual transaction completion
-
Allows backend verification before finalizing
-
Prevents accidental subscription activation without server confirmation
appAccountToken: Links purchase to specific child account type: 'subs': Specifies subscription (not consumable/non-consumable)
Flow:
- Check App Store connection
- Show loading state
- Request purchase from App Store
- App Store shows payment sheet
- Transaction result handled separately (via currentPurchase listener)
3. Purchase Error Handling
const handlePurchaseError = (error: PurchaseError) => {
switch (error.code) {
case 'E_USER_CANCELLED':
// User cancelled - no action needed (silent)
break;
case 'E_ALREADY_OWNED':
toast.show({
text: t('alreadySubscribed'),
});
break;
default:
toast.show({
text: error.message || tError('error'),
});
break;
}
};
Error Codes:
- E_USER_CANCELLED: User closed payment sheet (no toast needed)
- E_ALREADY_OWNED: Subscription already purchased (inform user)
- Default: Generic error with error message fallback
Purpose: Provides user-friendly error messages without overwhelming users with cancellations
🔧 Technical Implementation Details
Key Libraries and Dependencies
Android Implementation:
react-native-razorpay: Razorpay payment gateway integration@gql/graphql: GraphQL client for API communication@shopify/restyle: Utility-first styling frameworkdayjs: Date manipulation libraryreact-native-safe-area-context: Safe area handling
iOS Implementation:
expo-iap: In-app purchase integration for iOS@gql/graphql: GraphQL client for API communication@shopify/restyle: Utility-first styling frameworkdayjs: Date manipulation libraryreact-native-safe-area-context: Safe area handling
Platform-Specific Considerations
Android:
- Razorpay integration requires proper key configuration
- Payment status handling with comprehensive error management
- Subscription lifecycle management through Razorpay APIs
- Platform-specific UI components and styling
iOS:
- App Store integration requires proper product configuration
- In-app purchase validation with comprehensive error handling
- Subscription group management for different product tiers
- Native iOS subscription experience with App Store integration
Error Handling and Status Management
Both platforms implement comprehensive error handling with:
- Payment status tracking (Processing, Success, Failed)
- User-friendly error messages and retry mechanisms
- Comprehensive logging for debugging and monitoring
- Graceful fallback handling for network issues
Security and Compliance
- Secure payment processing with platform-specific encryption
- PCI DSS compliance for payment data handling
- Secure API communication with proper authentication
- Platform-specific security best practices implementation
🏗️ Architecture
Payment System Components
Payment & Subscription System
├── Subscription Plans
├── Payment Processing
├── Transaction Management
├── Platform Integration
├── Subscription Analytics
└── Invoice Generation
Data Flow
Subscription Selection → Payment Processing → Platform Integration → Confirmation → Database Update
↓ ↓ ↓ ↓ ↓
Plan Selection → Razorpay/iOS/Android → Payment Gateway → Success/Failure → Status Update
📱 Mobile Implementation
Subscription Management
1. Context and State Handling
const { activeChildId, setActiveChildId } = useChildContext();
const [currentlyOpenUpgradePlan, setCurrentOpenUpgradePlan] = useState<any>(null);
const [currentlyOpenSubscriptionPlan, setCurrentOpenSubscriptionPlan] = useState<any>(null);
const [showCancelModal, setShowCancelModal] = useState(false);
Manages child context for switching between children.
Controls UI states for:
Opening upgrade and subscription plan modals.
Displaying cancellation confirmation modal.
2. Fetching Subscription Plans
const { data: subscriptionPlansData } = useGetSubscriptionPlansQuery({
variables: { platform: Subscription_Platform_Enum?.Android },
fetchPolicy: 'cache-and-network',
});
Retrieves available subscription plans for the given platform (Android).
Uses cache-and-network for optimized loading — shows cached data first, then updates from network.
3. Fetching Parent & Child Information
const { data: parentChildData } = useGetParentChildsTherapistsQuery({
variables: { parent_id: userData?.parentId },
fetchPolicy: 'network-only',
});
Fetches list of children associated with the parent.
Ensures latest data from the server using network-only.
4. Selecting Active Child
const selectedChild = React.useMemo(
() => parentChildData?.child.find(c => c.id === activeChildId) ?? null,
[parentChildData?.child, activeChildId],
);
Efficiently determines the currently selected child.
Uses useMemo to avoid unnecessary recalculations unless child data or ID changes.
5. Fetching Child Subscriptions
const { data: childSubscriptionsData, refetch: refetchChildSubscriptionsData } =
useGetChildSubscriptionsQuery({
variables: { child_id: activeChildId },
fetchPolicy: 'cache-and-network',
});
Retrieves subscription details for the active child.
Allows refetch to refresh data after upgrades, cancellations, or renewals.
6. Detecting Active Subscription Plan
const currentlyActivePlan = (id: any) => {
return (
id ===
childSubscriptionsData?.child_subscription.find(
sub => sub.subscription_status === 'ACTIVE',
)?.plan_id
);
};
Core logic for checking if a plan is currently active.
Compares a given plan ID with the child’s active subscription (subscription_status === 'ACTIVE').
iOS Subscription Management
1. State Management
const [iosSubscriptionOfferings, setIosSubscriptionOfferings] = useState<PricingPlan[]>([]);
const [freePlanData, setFreePlanData] = useState([]);
const [customerInfo, setCustomerInfo] = useState<null>(null);
Purpose: Stores iOS subscription offerings, free plan data, and customer purchase info
2. Data Fetching Queries
Parent's Children with Active Subscriptions
const { data: parentChildData } = useGetParentChildsActiveSubcriptionQuery({
variables: { parent_id: userData?.parentId },
fetchPolicy: 'network-only',
});
Purpose: Gets all children and their active subscriptions for the parent Subscription Group Names
const { data: subscriptionGroupNames } = useGetSubscriptionGroupNamesQuery({
fetchPolicy: 'network-only',
});
Purpose: Fetches iOS subscription group identifiers (required for App Store product groups) Child Subscriptions
const { data: childSubscriptionsData, refetch } = useGetChildSubscriptionsQuery({
variables: { child_id: activeChildId },
fetchPolicy: 'cache-and-network',
});
Purpose: Gets specific child's subscription history and status
3. IAP Hook Integration
const {
connected, // Store connection status
subscriptions, // Available products
fetchProducts, // Load products from App Store
getAvailablePurchases, // Get purchase history
hasActiveSubscriptions, // Check active status
activeSubscriptions, // Active subscription list
currentPurchase, // Latest purchase
currentPurchaseError, // Purchase errors
finishTransaction, // Complete transaction
} = useIAP();
Purpose: React Native IAP hook for App Store operations
4. Confirm Plan Mutation
const [confirmPlanMutation, { loading: confirmLoading }] = useConfirmPlanMutation({
onCompleted: (data) => {
if (data.confirmPlan.success) {
toast.show({ text: 'Subscription activated successfully' });
refetchChildSubscriptionsData(); // Refresh subscription data
} else {
toast.show({ text: data.confirmPlan.message || 'Failed to activate subscription' });
}
},
onError: (error) => {
toast.show({ text: error.message || 'Failed to activate subscription' });
}
});
- Purpose: Sends purchase details to backend for verification and activation
- Success: Shows success message and refreshes child subscriptions
- Failure: Displays error message from backend or generic error
5. Purchase Handler
const handleBuySubscription = async (product: any) => {
try {
setPurchaseLoading(true);
// Step 1: Purchase from App Store
const purchase = await purchaseProduct(product.productId);
// Step 2: Verify with backend if purchase successful
if (purchase) {
await confirmPlanMutation({
variables: {
obj: {
child_id: activeChildId,
ios_product_id: product.productId,
ios_transaction_id: purchase.transactionId,
subscription_end: purchase.expirationDate,
subscription_start: purchase.purchaseTime,
subscription_platform: 'IOS',
},
},
});
}
} catch (error) {
console.error('iOS purchase error:', error);
toast.show({ text: 'Purchase failed' });
} finally {
setPurchaseLoading(false);
}
};
Purchase Flow:
- Set loading state to disable UI during purchase
- Call App Store to initiate purchase (shows Apple payment sheet)
- Verify purchase exists after user completes payment
- Send to backend with transaction details:
Child ID (who the subscription is for) Product ID (which subscription) Transaction ID (unique purchase identifier) Start & end dates (subscription period) Platform identifier (iOS)
Backend validates with Apple's servers Activate subscription in database Refresh UI with new subscription status
Android Subscription Management
1. State Management
const [androidProducts, setAndroidProducts] = useState([]);
const [purchaseLoading, setPurchaseLoading] = useState(false);
Purpose: Stores Android subscription products and purchase loading state
2. Platform-Specific Query
const { data: subscriptionPlansData } = useGetSubscriptionPlansQuery({
variables: {
platform: Subscription_Platform_Enum?.Android,
},
fetchPolicy: 'cache-and-network',
});
- Purpose: Fetches only Android subscription plans from backend
- Filter: Uses platform enum to get Android-specific products
- Fetch Policy: Shows cached data first, then updates from network
3. IAP Hook Integration
const {
connected, // Play Store connection status
subscriptions, // Available products
fetchProducts, // Load products from Play Store
getAvailablePurchases, // Get purchase history
hasActiveSubscriptions, // Check active status
activeSubscriptions, // Active subscription list
currentPurchase, // Latest purchase
currentPurchaseError, // Purchase errors
finishTransaction, // Complete transaction
} = useIAP();
Purpose: React Native IAP hook for Google Play Store operations
4. Confirm Plan Mutation
const [confirmPlanMutation, { loading: confirmLoading }] = useConfirmPlanMutation({
onCompleted: (data) => {
if (data.confirmPlan.success) {
toast.show({ text: 'Subscription activated successfully' });
refetchChildSubscriptionsData();
} else {
toast.show({ text: data.confirmPlan.message || 'Failed to activate subscription' });
}
},
onError: (error) => {
toast.show({ text: error.message || 'Failed to activate subscription' });
}
});
Purpose: Sends purchase details to backend for Google Play verification Success: Activates subscription and refreshes data Failure: Shows error message
5. Android Purchase Handler
const handleBuySubscription = async (product: any) => {
try {
setPurchaseLoading(true);
// Step 1: Purchase from Google Play Store
const purchase = await purchaseProduct(product.productId);
// Step 2: Verify with backend if purchase successful
if (purchase) {
await confirmPlanMutation({
variables: {
obj: {
child_id: activeChildId,
android_product_id: product.productId,
android_purchase_token: purchase.purchaseToken, // Android-specific
subscription_end: purchase.expirationDate,
subscription_start: purchase.purchaseTime,
subscription_platform: 'ANDROID',
},
},
});
}
} catch (error) {
console.error('Android purchase error:', error);
toast.show({ text: 'Purchase failed' });
} finally {
setPurchaseLoading(false);
}
};
Purchase Flow:
-
Set loading state to prevent multiple purchases
-
Call Google Play Store to initiate purchase (shows Google payment sheet)
-
Verify purchase exists after payment completion
-
Send to backend with Android-specific data:
- Child ID (subscription owner)
- Android Product ID (SKU from Play Console)
- Purchase Token (unique identifier from Google Play)
- Start & end dates (subscription period)
- Platform identifier (ANDROID)
-
Backend validates with Google Play's API
-
Activate subscription in database
-
Refresh UI with updated status
💰 Transaction Management
Transaction History
1. Filter and Pagination State Management
const [offset, setOffset] = useState(0);
const [modalVisible, setModalVisible] = useState(false);
const [filter, setFilter] = useState<Filter>({
status: [],
dateRange: { from: undefined, to: undefined },
type: [],
});
Manages UI filters (status, type, date range) and pagination.
offset → used for paginated queries.
modalVisible → toggles filter modal visibility.
2. Filter Application Logic
const isFilterApplied =
filter?.status?.length > 0 ||
filter?.type?.length > 0 ||
(filter?.dateRange?.from && filter?.dateRange?.to);
Determines whether any filter is currently active.
Useful for updating UI (e.g., showing “Clear Filters” button).
3. Pagination and Query Setup
const ITEMS_PER_PAGE = 10;
const whereCondition: Payment_Bool_Exp = useMemo(() => {
return {
status: { _is_null: false },
};
}, []);
Sets a default limit of 10 items per page.
Filters out invalid transactions (status must not be null).
Memoized for performance using useMemo.
4. Fetching Parent Transactions
const data = useGetParentTransactionsQuery({
variables: {
offset: 0,
whereCondition,
limit: ITEMS_PER_PAGE,
},
fetchPolicy: 'cache-and-network',
});
Fetches transaction history using GraphQL.
Combines cached data with fresh updates (cache-and-network).
Supports pagination via offset and limit.
5. Parsing Other Charges
const getOtherCharges = (charges: string | Array<other_charge>): Array<other_charge> => {
if (typeof charges === 'string') {
try {
const parseCharges = JSON.parse(charges);
return parseCharges;
} catch (error) {
console.error('Error parsing other charges:', error);
return [];
}
}
return charges || [];
};
Safely handles “other charges” which can be stored as:
JSON string, or
Direct array of other_charge objects.
Ensures consistent output even if parsing fails.
6. Invoice Generation
const generateInvoice = async (
transactionDetails: GetParentTransactionsQuery['payment'][number],
) => {
try {
const invoiceData = {
transactionId: transactionDetails.id,
amount: transactionDetails.amount,
date: transactionDetails.created_at,
description: transactionDetails.description,
otherCharges: getOtherCharges(transactionDetails.other_charges),
totalAmount: transactionDetails.total_amount,
status: transactionDetails.status,
paymentMethod: transactionDetails.payment_method,
childName: transactionDetails.child?.name,
parentName: transactionDetails.parent?.user?.name,
};
await generateInvoiceIOS(invoiceData);
toast.show({ text: 'Invoice generated successfully' });
} catch (error) {
console.error('Error generating invoice:', error);
toast.show({ text: 'Failed to generate invoice' });
}
};
Builds invoice data from transaction details.
Handles nested data for child and parent names.
Uses platform-specific generateInvoiceIOS() to generate the file.
Displays success or failure feedback using a toast.
🔄 Payment Processing
Razorpay Integration
1. Purpose
The useRazorpayPayment hook handles Razorpay checkout integration and manages payment state within the app.
It provides a reusable, isolated logic block for initiating and tracking payments.
2. State Management
const [paymentLoading, setPaymentLoading] = useState(false);
Tracks the payment process state.
Prevents duplicate payment attempts and allows displaying a loading indicator in the UI.
3. Main Function – processRazorpayPayment
const processRazorpayPayment = async (paymentData: RazorpayPaymentData) => {
setPaymentLoading(true);
try {
const result = await RazorpayCheckout.open({
description: paymentData.description,
image: 'https://your-logo-url.com/logo.png',
currency: 'INR',
key: process.env.EXPO_PUBLIC_RAZORPAY_KEY,
amount: paymentData.amount * 100,
name: 'Com DEALL',
order_id: paymentData.order_id,
prefill: {
email: paymentData.email,
contact: paymentData.phone,
name: paymentData.name,
},
theme: { color: '#F37254' }
});
return result;
} catch (error) {
console.error('Razorpay payment error:', error);
throw error;
} finally {
setPaymentLoading(false);
}
};
🧠 Explanation:
-
Starts the Razorpay checkout flow with all required parameters:
-
order_id: Generated from backend for secure payment initiation. -
amount: Converted to paise (amount * 100) as per Razorpay requirement. -
prefill: Pre-populates user information for faster checkout. -
theme: Customizes the payment screen’s color scheme.
Handles errors gracefully, logging them and rethrowing for higher-level handling.
Resets loading state after completion (both success and failure).
4. Returned Values
return {
processRazorpayPayment,
paymentLoading
};
Exposes:
-
processRazorpayPayment: Function to trigger payment.
-
paymentLoading: Boolean flag to show loading UI during payment.
iOS In-App Purchase
// iOS in-app purchase handling
const useIOSInAppPurchase = () => {
const [purchaseLoading, setPurchaseLoading] = useState(false);
const {
connected,
subscriptions,
fetchProducts,
getAvailablePurchases,
hasActiveSubscriptions,
activeSubscriptions,
getActiveSubscriptions,
finishTransaction,
currentPurchase,
currentPurchaseError,
} = useIAP();
const handlePurchase = async (productId: string) => {
setPurchaseLoading(true);
try {
const result = await purchaseProduct(productId);
if (result) {
await finishTransaction(result);
return result;
}
} catch (error) {
console.error('iOS purchase error:', error);
throw error;
} finally {
setPurchaseLoading(false);
}
};
const handleRestorePurchases = async () => {
try {
const purchases = await getAvailablePurchases();
return purchases;
} catch (error) {
console.error('Restore purchases error:', error);
throw error;
}
};
return {
handlePurchase,
handleRestorePurchases,
purchaseLoading,
connected,
subscriptions,
hasActiveSubscriptions,
activeSubscriptions
};
};
Android In-App Purchase
// Android in-app purchase handling
const useAndroidInAppPurchase = () => {
const [purchaseLoading, setPurchaseLoading] = useState(false);
const {
connected,
subscriptions,
fetchProducts,
getAvailablePurchases,
hasActiveSubscriptions,
activeSubscriptions,
getActiveSubscriptions,
finishTransaction,
currentPurchase,
currentPurchaseError,
} = useIAP();
const handlePurchase = async (productId: string) => {
setPurchaseLoading(true);
try {
const result = await purchaseProduct(productId);
if (result) {
await finishTransaction(result);
return result;
}
} catch (error) {
console.error('Android purchase error:', error);
throw error;
} finally {
setPurchaseLoading(false);
}
};
const handleRestorePurchases = async () => {
try {
const purchases = await getAvailablePurchases();
return purchases;
} catch (error) {
console.error('Restore purchases error:', error);
throw error;
}
};
return {
handlePurchase,
handleRestorePurchases,
purchaseLoading,
connected,
subscriptions,
hasActiveSubscriptions,
activeSubscriptions
};
};
📊 Subscription Analytics
Subscription Statistics
// Subscription analytics for parents
const useSubscriptionAnalytics = (parentId: string) => {
const { data: analyticsData } = useGetParentSubscriptionAnalyticsQuery({
variables: {
parent_id: parentId,
date_range: {
start: format(subDays(new Date(), 90), 'YYYY-MM-DD'),
end: format(new Date(), 'YYYY-MM-DD')
}
}
});
const analytics = useMemo(() => {
if (!analyticsData) return null;
return {
totalSubscriptions: analyticsData.total_subscriptions,
activeSubscriptions: analyticsData.active_subscriptions,
expiredSubscriptions: analyticsData.expired_subscriptions,
totalSpent: analyticsData.total_spent,
averageSubscriptionDuration: analyticsData.average_duration,
popularPlans: analyticsData.popular_plans,
monthlyTrend: analyticsData.monthly_trend,
renewalRate: analyticsData.renewal_rate
};
}, [analyticsData]);
return analytics;
};
📱 Mobile-Specific Features
Touch Interactions
// Payment touch interactions
const usePaymentGestures = () => {
const handleSwipeLeft = (transaction: Transaction) => {
// Swipe left to generate invoice
generateInvoice(transaction);
};
const handleSwipeRight = (transaction: Transaction) => {
// Swipe right to view details
showTransactionDetails(transaction);
};
const handleLongPress = (subscription: Subscription) => {
// Long press to show subscription options
showSubscriptionOptions(subscription);
};
return {
handleSwipeLeft,
handleSwipeRight,
handleLongPress
};
};
Usage Flow
- Offline: Payment saved locally with isOffline: true flag
- Back Online: syncOfflinePayments() sends all queued payments to server
- Success: Local queue cleared, user notified
- Failure: Payments remain in queue for retry
Storage
- Uses MMKV for persistent local storage
- Payments survive app restarts
- Synced automatically when connection restored
Offline Payment Management
// Offline payment capabilities
const useOfflinePayments = () => {
const [offlinePayments, setOfflinePayments] = useState([]);
const saveOfflinePayment = (payment: Payment) => {
const offlinePayment = {
...payment,
id: `offline_${Date.now()}`,
isOffline: true,
createdAt: new Date().toISOString()
};
setOfflinePayments(prev => [...prev, offlinePayment]);
mmkvStorage.set('offline_payments', JSON.stringify([...offlinePayments, offlinePayment]));
};
const syncOfflinePayments = async () => {
if (offlinePayments.length === 0) return;
try {
for (const payment of offlinePayments) {
await processPayment(payment);
}
setOfflinePayments([]);
mmkvStorage.delete('offline_payments');
toast.show({ text: 'Offline payments synced successfully' });
} catch (error) {
toast.show({ text: 'Failed to sync offline payments' });
}
};
return {
saveOfflinePayment,
syncOfflinePayments,
offlinePayments
};
};
🎨 UI Components
Subscription Plan Card
// SubscriptionPlanCard.tsx
const SubscriptionPlanCard = ({
plan,
isActive,
onSelect,
onUpgrade
}) => {
const { colors } = useTheme<Theme>();
return (
<TouchableOpacity
style={[
styles.planCard,
{ borderColor: isActive ? colors.primary36 : colors.backgroundStatusE9 }
]}
onPress={() => onSelect(plan)}
>
<Box flexDirection="row" justifyContent="space-between" alignItems="center">
<Box flex={1}>
<Text variant="heading5">{plan.name}</Text>
<Text variant="body2" color="secondary63">
{plan.description}
</Text>
<Text variant="heading4" color="primary36">
₹{plan.price}/{plan.duration}
</Text>
</Box>
<Box alignItems="flex-end">
{isActive && (
<Box
backgroundColor="primary36"
paddingHorizontal="s"
paddingVertical="xxs"
borderRadius="s"
>
<Text variant="caption" color="white">Active</Text>
</Box>
)}
{!isActive && (
<TouchableOpacity
onPress={() => onUpgrade(plan)}
style={styles.upgradeButton}
>
<Text color="primary36">Upgrade</Text>
</TouchableOpacity>
)}
</Box>
</Box>
<Box marginTop="m">
<Text variant="body2" color="secondary63">
Features:
</Text>
{plan.features.map((feature, index) => (
<Text key={index} variant="caption" color="secondary63">
• {feature}
</Text>
))}
</Box>
</TouchableOpacity>
);
};
🔧 GraphQL Integration
Subscription Queries
# Get subscription plans
query GetSubscriptionPlans($platform: subscription_platform_enum!) {
subscription_plans(
where: { platform: { _eq: $platform } }
order_by: { price: asc }
) {
id
name
description
price
duration
platform
features
benefits
}
}
# Get child subscriptions
query GetChildSubscriptions($child_id: uuid!) {
child_subscription(
where: { child_id: { _eq: $child_id } }
order_by: { created_at: desc }
) {
id
subscription_status
subscription_start
subscription_end
plan {
id
name
price
duration
}
}
}
Payment Mutations
# Confirm subscription payment
mutation ConfirmPlan($obj: child_subscription_insert_input!) {
confirmPlan(obj: $obj) {
success
message
subscription_id
}
}
# Cancel subscription
mutation CancelSubscription($subscription_id: uuid!, $reason: String!) {
cancelSubscription(
subscription_id: $subscription_id
reason: $reason
) {
success
message
}
}
🎯 Best Practices
Payment Management
- Security: Secure payment processing with proper validation
- Platform Integration: Seamless platform-specific payment handling
- Transaction Tracking: Comprehensive transaction history
- Invoice Generation: Automated invoice generation
User Experience
- Visual Feedback: Clear payment status indicators
- Touch Interactions: Intuitive payment gestures
- Offline Support: Allow offline payment management
- Real-time Updates: Live payment status synchronization
Performance
- Payment Optimization: Fast payment processing
- Data Caching: Cache payment data locally
- Lazy Loading: Load transactions on demand
- Background Sync: Sync payments in background