Skip to main content

💳 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

  1. 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
  1. 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

  1. 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)

  1. 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

  1. 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

  1. 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

  1. 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

  1. 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 framework
  • dayjs: Date manipulation library
  • react-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 framework
  • dayjs: Date manipulation library
  • react-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:

  1. Set loading state to prevent multiple purchases

  2. Call Google Play Store to initiate purchase (shows Google payment sheet)

  3. Verify purchase exists after payment completion

  4. 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)
  5. Backend validates with Google Play's API

  6. Activate subscription in database

  7. 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

🎯 Next Steps