Skip to content

Instantly share code, notes, and snippets.

@axmad386
Created January 15, 2026 02:47
Show Gist options
  • Select an option

  • Save axmad386/f9fe150b78384cf331cdc5d282bd1552 to your computer and use it in GitHub Desktop.

Select an option

Save axmad386/f9fe150b78384cf331cdc5d282bd1552 to your computer and use it in GitHub Desktop.
In App Purchase Setup for Ios and Android

Real Credentials Setup & Integration Testing Guide

Status: Setup required before production deployment
Date: December 31, 2025
Version: 1.0


πŸ“‹ Table of Contents

  1. Overview
  2. Google Play Billing Setup
  3. Apple App Store Setup
  4. Environment Variables Configuration
  5. Sandbox Integration Testing
  6. Webhook Configuration
  7. Production Deployment
  8. Monitoring & Verification
  9. Troubleshooting

🎯 Overview

This guide covers:

  • Setting up real API credentials for Google Play & Apple App Store
  • Testing with sandbox/staging credentials (NOT production)
  • Integrating webhooks for real notifications
  • Verification before production rollout

Timeline: 2-3 hours for complete setup

Risk Level: LOW (using sandbox, won't charge real money)


πŸ” Google Play Billing Setup

Step 1: Create Google Cloud Project

# Visit Google Cloud Console
https://console.cloud.google.com/

# Create new project or use existing
Project Name: "LongEage IAP"

Step 2: Enable APIs

Services to enable:
1. Google Play Android Developer API
2. Google Cloud Pub/Sub API
3. Service Usage API

How to enable:

1. Go to APIs & Services > Library
2. Search for "Google Play Android Developer API"
3. Click "Enable"
4. Repeat for Cloud Pub/Sub API

Step 3: Create Service Account

# In Google Cloud Console
APIs & Services > Credentials > Create Credentials > Service Account

Service Account Name: "longeage-iap-service"
Grant roles:
  - Editor (for testing)
  - Pub/Sub Editor (for webhooks)

Step 4: Create & Download Service Account Key

# After creating service account
1. Click on the service account
2. Go to "Keys" tab
3. Click "Add Key" > "Create new key"
4. Choose JSON format
5. Download file: `service-account-key.json`

File content should look like:
{
  "type": "service_account",
  "project_id": "longeage-iap-...",
  "private_key_id": "...",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...",
  "client_email": "longeage-iap-service@...",
  "client_id": "...",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "..."
}

Step 5: Add App to Google Play Console

Link your Android app to the service account:

Google Play Console > App Settings > Users and Permissions
1. Invite user with email: longeage-iap-service@[PROJECT_ID].iam.gserviceaccount.com
2. Grant role: "Admin"
3. Accept invitation (via service account email)

Note: Service account won't accept invitations via email.
      Use OAuth flow or create key in Google Cloud, not Play Console.

Step 6: Setup Pub/Sub for Webhooks

# In Google Cloud Console
Pub/Sub > Topics > Create Topic

Topic Name: "longeage-iap-notifications"

# Then create subscription
Subscriptions > Create Subscription

Subscription Name: "longeage-iap-webhook"
Topic: "longeage-iap-notifications"
Delivery Type: "Push"
Push Endpoint: "https://your-backend.com/webhooks/iap/google"
(Replace with your actual backend URL)

🍎 Apple App Store Setup

Step 1: Get App ID

Developer Account > Apps > Your App > App ID (Bundle ID)
Example: com.longeage.app

Step 2: Create Shared Secret

App Store Connect > Your App > In-App Purchases

1. Scroll to "App-Specific Shared Secret"
2. If not created, click "Generate"
3. Copy the secret (long alphanumeric string)

Save as: APPLE_SHARED_SECRET in .env

Step 3: Configure Webhooks

Developer Account > Apps > Your App > App Information

Scroll to "App Store Server Notifications"

1. Event notification endpoint: https://your-backend.com/webhooks/iap/apple
2. Check: "Server Notifications Version 2" (important!)
3. Save

Step 4: Get Server to Server Token (Optional, for verification API)

If you want to verify receipts server-to-server (not needed for webhooks):

Developer Account > Keys
1. Create new key: "Longeage IAP Verification"
2. Select "In-App Purchase" permissions
3. Download key file (named: AuthKey_[KEY_ID].p8)
4. Save somewhere safe

Key details to note:

- Key ID: [KEY_ID] from filename
- Team ID: Found in Account > Membership > Team ID
- Bundle ID: Your app's bundle ID

πŸ”§ Environment Variables Configuration

Update .env file

# Google Play
GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account-key.json"
GOOGLE_PLAY_PACKAGE_NAME="com.longeage.app"
GOOGLE_PUBSUB_TOPIC="projects/[PROJECT_ID]/topics/longeage-iap-notifications"
GOOGLE_PUBSUB_SUBSCRIPTION="longeage-iap-webhook"

# Apple
APPLE_SHARED_SECRET="[Your Shared Secret from App Store Connect]"
APPLE_BUNDLE_ID="com.longeage.app"
APPLE_TEAM_ID="[Your Team ID]"
APPLE_KEY_ID="[Your Key ID if using server verification]"

# IAP Mode (IMPORTANT!)
IAP_MODE="REAL"  # Change from MOCK when ready

# Webhook URLs
WEBHOOK_GOOGLE_URL="https://your-backend.com/webhooks/iap/google"
WEBHOOK_APPLE_URL="https://your-backend.com/webhooks/iap/apple"

Verify Environment Variables

# Check if variables are loaded
npm run dev

# In your backend logs, you should see:
# βœ… IAP Mode: REAL
# βœ… Google Play Package: com.longeage.app
# βœ… Apple Bundle ID: com.longeage.app

πŸ§ͺ Sandbox Integration Testing

Phase 1: Verify Google Play API Connection

Test Script: Create scripts/test-iap-google.ts

import { google } from "googleapis";

async function testGooglePlayAPI() {
  try {
    const auth = new google.auth.GoogleAuth({
      keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS,
      scopes: ["https://www.googleapis.com/auth/androidpublisher"],
    });

    const androidpublisher = google.androidpublisher({
      version: "v3",
      auth,
    });

    // Test with a sandbox product
    const response = await androidpublisher.purchases.subscriptions.get({
      packageName: process.env.GOOGLE_PLAY_PACKAGE_NAME!,
      subscriptionId: "longeage_premium_1m_sandbox", // Use sandbox product
      token: "SANDBOX_TEST_TOKEN", // Special token for testing
    });

    console.log("βœ… Google Play API connection successful");
    console.log("Response:", response.data);
  } catch (error) {
    console.error("❌ Google Play API error:", error);
  }
}

testGooglePlayAPI();

Run:

npx ts-node scripts/test-iap-google.ts

Expected Result:

βœ… Google Play API connection successful
Response: { ... }

Phase 2: Verify Apple API Connection

Test Script: Create scripts/test-iap-apple.ts

import https from "https";

async function testAppleAPI() {
  const receiptData = Buffer.from("SANDBOX_RECEIPT_DATA_HERE").toString(
    "base64",
  );

  const payload = JSON.stringify({
    "receipt-data": receiptData,
    password: process.env.APPLE_SHARED_SECRET,
    "exclude-old-transactions": true,
  });

  return new Promise<void>((resolve, reject) => {
    const options = {
      hostname: "sandbox.itunes.apple.com", // Use sandbox!
      path: "/verifyReceipt",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Content-Length": payload.length,
      },
    };

    const req = https.request(options, (res) => {
      let data = "";
      res.on("data", (chunk) => {
        data += chunk;
      });
      res.on("end", () => {
        const response = JSON.parse(data);
        if (response.status === 0) {
          console.log("βœ… Apple API connection successful");
          console.log("Response:", response);
          resolve();
        } else {
          console.error(
            `❌ Apple API error: Status ${response.status}`,
            response,
          );
          reject(new Error(`Status: ${response.status}`));
        }
      });
    });

    req.on("error", reject);
    req.write(payload);
    req.end();
  });
}

testAppleAPI().catch(console.error);

Run:

# First, get a sandbox receipt from your test app
# Then replace SANDBOX_RECEIPT_DATA_HERE with actual receipt

npx ts-node scripts/test-iap-apple.ts

Expected Result:

βœ… Apple API connection successful
Response: { status: 0, ... }

Phase 3: Test via API Endpoint

# 1. Create test user in database
# (Use your actual user ID from testing app)

# 2. Get JWT token for test user
# (See /auth/login endpoint)

# 3. Test POST /iap/verify with real token from sandbox app

curl -X POST http://localhost:3000/iap/verify \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "ANDROID",
    "packageName": "com.longeage.app",
    "productId": "longeage_premium_1m",
    "token": "SANDBOX_PURCHASE_TOKEN_FROM_APP"
  }'

# Expected response:
{
  "success": true,
  "data": {
    "success": true,
    "status": "ACTIVE",
    "expiryDate": "2025-01-30T12:34:56.789Z",
    "isPremiumNow": true,
    "daysRemaining": 30
  }
}

Phase 4: Test Cron Job

# Manually trigger cron job (for testing)

# Option 1: Create admin endpoint
POST /admin/iap/trigger-cleanup
Authorization: Bearer ADMIN_TOKEN

# Response:
{
  "success": true,
  "marked": 0,  // No expired subscriptions in sandbox
  "message": "Marked 0 subscriptions as EXPIRED"
}

# Option 2: Wait for scheduled cron at 00:00 UTC
# Check logs for:
# "πŸ•΅οΈ [CRON] Daily subscription expiration cleanup..."

πŸͺ Webhook Configuration

Google Cloud Pub/Sub Webhook

1. Create Pub/Sub subscription (already done in setup)

2. Configure endpoint

# Your backend must accept POST requests at:
# POST /webhooks/iap/google

# Header validation:
# Google sends Authorization header (optional, validate if needed)

# Body format:
{
  "message": {
    "data": "base64_encoded_json",
    "messageId": "...",
    "publishTime": "..."
  }
}

# Decoded data:
{
  "packageName": "com.longeage.app",
  "subscriptionId": "longeage_premium_1m",
  "subscriptionNotification": {
    "version": "1",
    "notificationType": 2,  // 2 = RENEWAL
    "purchaseToken": "token_xyz"
  }
}

3. Test webhook locally

# Start your backend in REAL mode
npm run dev

# Send test notification via curl
curl -X POST http://localhost:3000/webhooks/iap/google \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "data": "eyJwYWNrYWdlTmFtZSI6ImNvbS5sb25nZWFnZS5hcHAiLCJzdWJzY3JpcHRpb25JZCI6ImxvbmdlYWdlX3ByZW1pdW1fMW0iLCJzdWJzY3JpcHRpb25Ob3RpZmljYXRpb24iOnsidmVyc2lvbiI6IjEiLCJub3RpZmljYXRpb25UeXBlIjoyLCJwdXJjaGFzZVRva2VuIjoiLi4uIn19"
      }
    }'

# Check logs for:
# "βœ… Google Pub/Sub webhook processed"

Apple Webhook Configuration

1. Register endpoint in App Store Connect

Already done in Apple setup section:
App Store Connect > Your App > App Information > App Store Server Notifications

Endpoint: https://your-backend.com/webhooks/iap/apple
Version: 2 (required!)

2. Verify webhook signature (important for production)

// In iap.webhook.controller.ts
// Validate Apple signature using:
// https://developer.apple.com/documentation/appstoreserverapi/app_store_server_notifications

import { verifyJWT } from 'apple-iap-verifier';  // or similar

async handleAppleWebhook(body: { signedPayload: string }) {
  // 1. Verify JWT signature using Apple's public key
  // 2. Decode payload
  // 3. Process notification
}

3. Test webhook locally

# For testing, you can skip signature verification
# (add flag: IAP_SKIP_APPLE_VERIFICATION=true in .env for tests)

# Send test notification
curl -X POST http://localhost:3000/webhooks/iap/apple \
  -H "Content-Type: application/json" \
  -d '{
    "signedPayload": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlCOHpDQ0EzeWdBd0lCQWdJUWZtOGlaWnp1cTBFSkFBQUFBSDd1UlRBTkJna3Foa2lHOXcwQkFRc0ZBREEzTVJzd0dRWURWUVFERXhwcmRXSmxjbTVsY3kxdGJ5MU5SVEF3Wm5SNVNtRkFNQjRYRFRJeFl6RXlOekEwTkRReE1Wb1hEVEkxWXpFNU5qYzBNVGRHTVRFd01UQkJCZ05WQkFFVEMwVlRVeEZXRVZUSWdjQkVnSktiV0ZzZFd5RWN3... (very long JWT)"
  }'

# Check logs for:
# "βœ… Apple webhook processed: DID_RENEW"

πŸš€ Production Deployment

Pre-Production Checklist

# 1. Verify all credentials are in place
echo $APPLE_SHARED_SECRET
echo $GOOGLE_APPLICATION_CREDENTIALS
# Both should return values (not empty)

# 2. Verify IAP_MODE is set to REAL
echo $IAP_MODE  # Should print: REAL

# 3. Run integration tests
npm run test -- src/client/iap/iap.spec.ts
# All 4 tests should pass

# 4. Build production bundle
npm run build
# Should complete with 0 errors

# 5. Check for any hardcoded sandbox URLs
grep -r "sandbox.itunes" src/
# Should return NO results (use dynamic endpoint based on env)

# 6. Verify webhook URLs are production domains
grep -r "localhost" src/client/iap/
# Should return NO results

Deployment Steps

# 1. Merge to production branch
git checkout production
git pull origin main
git push origin production

# 2. Deploy to production environment
# (Use your CI/CD pipeline)

# Docker example:
docker build -t longeage-app:iap-v1 .
docker push [YOUR_REGISTRY]/longeage-app:iap-v1

# Kubernetes example:
kubectl set image deployment/longeage-app \
  longeage=longeage-app:iap-v1 \
  -n production

# 3. Verify deployment
curl https://api.longeage.com/health
# Should return 200 OK

# 4. Check logs
kubectl logs -f deployment/longeage-app -n production | grep IAP
# Should show: "IAP Mode: REAL"

Post-Deployment Verification

# 1. Monitor webhook delivery
# Google Cloud Pub/Sub > Subscriptions > longeage-iap-webhook
# Check "Ack Message Count" increasing

# 2. Check Sentry for errors
# Sentry Dashboard > Issues
# Filter by "iap" tags
# Should be 0 errors

# 3. Verify premium users are working
# In app, test: GET /auth/me
# User with active subscription should have: is_premium: true

# 4. Monitor cron job execution
# Check application logs at 00:00 UTC daily
# Should see: "πŸ•΅οΈ [CRON] Daily subscription expiration cleanup..."

πŸ“Š Monitoring & Verification

Metrics to Track

1. Verification Success Rate
   └─ /iap/verify endpoint
   └─ Target: > 99%
   └─ Alert if < 95% for 5 minutes
   └─ Sentry: Monitor HTTP 400/500 responses

2. Webhook Processing Time
   └─ /webhooks/iap/google and /webhooks/iap/apple
   └─ Target: < 500ms average
   └─ Alert if > 2s
   └─ Metric: webhook_processing_time_ms

3. Premium Users Count
   └─ Query: SELECT COUNT(*) FROM user WHERE is_premium = true
   └─ Track daily trend
   └─ Alert if drops > 10% without reason

4. Subscription Status Distribution
   └─ ACTIVE count
   └─ EXPIRED count
   └─ CANCELLED count
   └─ Useful for churn analysis

5. Cron Job Execution
   └─ Scheduled daily at 00:00 UTC
   └─ Duration: Should be < 5 seconds
   └─ Alert if > 30 seconds

Dashboard Setup (Sentry/Datadog/CloudWatch)

Create dashboard with:
- IAP verification success rate (last 24h)
- Webhook processing time (p50, p95, p99)
- Premium user count (trend line)
- Cron job duration (last 7 days)
- Error rate by endpoint
- Database query performance

Log Aggregation

# Search for IAP logs
Sentry/ELK/CloudWatch: tags.module=iap

# Example queries:
- ERROR in module: iap
  └─ Shows all errors related to IAP

- metric: webhook_processing_time_ms > 2000
  └─ Shows slow webhook processing

- event: cron_failed
  └─ Shows failed cron executions

πŸ†˜ Troubleshooting

Issue 1: Google Play API Returns 401 Unauthorized

Symptoms:
- POST /iap/verify fails with 401
- Logs: "Invalid `googleAuth` signature"

Causes:
1. Service account credentials expired
2. Service account not added to Google Play Console
3. API not enabled in Google Cloud

Solutions:
1. Download new key from Google Cloud Console
2. Add service account email to Google Play Console > Settings > Users
3. Enable "Google Play Android Developer API" in Google Cloud > APIs

Test:

# Verify service account can access Google Play
curl -X GET \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.longeage.app/subscriptions

Issue 2: Apple API Returns Status 21007 (Test Receipt in Production)

Symptoms:
- POST /iap/verify with Apple receipt fails
- Logs: "Apple API returned status: 21007"

Cause:
- Using sandbox receipt in production endpoint
- OR using production receipt in sandbox endpoint

Solution:
- Automatic retry: Endpoint detects 21007 and retries sandbox
- This is expected behavior and will work
- No action needed

Issue 3: Webhook Not Being Delivered

Google Pub/Sub

Symptoms:
- /webhooks/iap/google not receiving requests
- Google Console shows "Ack Message Count" = 0

Checks:
1. Verify subscription exists
   └─ Google Cloud > Pub/Sub > Subscriptions > longeage-iap-webhook

2. Verify push endpoint is correct
   └─ Should be: https://your-backend.com/webhooks/iap/google
   └─ Check for typos, SSL certificate valid

3. Test message delivery
   └─ Go to Subscriptions > Publish test message
   └─ Should see POST request in backend logs

4. Check backend logs for errors
   └─ kubernetes logs | grep "webhook" | grep "ERROR"
   └─ Fix any parsing errors

5. Verify health check endpoint
   └─ curl https://your-backend.com/health
   └─ Should return 200 OK (Pub/Sub requires healthy endpoint)

Apple

Symptoms:
- /webhooks/iap/apple not receiving requests
- Sandbox app not triggering renewal

Checks:
1. Verify endpoint is registered
   └─ App Store Connect > Your App > App Information
   └─ Look for "App Store Server Notifications"
   └─ Check endpoint URL

2. Verify SSL certificate is valid
   └─ Apple requires valid, non-self-signed certificate
   └─ curl -I https://your-backend.com/webhooks/iap/apple
   └─ Should return 200 OK (or expected response)

3. Check backend logs
   └─ Should see incoming POST requests during renewal
   └─ If not, check iOS test app's renewal logic

4. Test from sandbox app
   └─ Trigger subscription renewal manually
   └─ Check backend logs within 10 seconds

Issue 4: Database Deadlock / Lock Timeout

Symptoms:
- POST /iap/verify sometimes fails with timeout
- Logs: "Transaction timeout exceeded"

Cause:
- Multiple concurrent requests for same user
- Database lock contention

Solution 1: Increase transaction timeout
  └─ iap.service.ts: Change timeout from 10s to 15s

Solution 2: Add request queuing
  └─ Use Bull queue to serialize /iap/verify per user
  └─ Prevents concurrent upsert on same subscription

Solution 3: Database optimization
  └─ Ensure index on (userId, originalTransactionId)
  └─ Run ANALYZE on subscription table

Check indexes:

-- In your database
SELECT indexname FROM pg_indexes
WHERE tablename = 'Subscription';

-- Should have indexes on:
-- - userId (for lookup)
-- - originalTransactionId (for uniqueness)
-- - status (for cron query)
-- - expiryDate (for cron range query)

Issue 5: Premium User Status Not Updating

Symptoms:
- User verifies purchase via /iap/verify
- Response shows isPremium: true
- But GET /auth/me still shows is_premium: false

Cause:
- User field not synced after subscription update
- Cache not invalidated

Solution:
1. Clear user cache (if using Redis)
   └─ redis-cli DEL user:{userId}

2. Refresh user session
   └─ Logout and login again
   └─ iOS/Android app should re-fetch via /auth/me

3. Force DB check
   └─ SELECT is_premium FROM "User" WHERE id = '{userId}'
   └─ Should show true if subscription.status = ACTIVE

βœ… Final Checklist Before Production

  • Google Play credentials downloaded and stored in .env
  • Apple Shared Secret created and stored in .env
  • Google Cloud Pub/Sub subscription created
  • Apple webhook endpoint registered in App Store Connect
  • All 4 integration tests pass
  • Build completes with 0 errors
  • No hardcoded sandbox URLs in code
  • Webhook URLs point to production domain
  • IAP_MODE environment variable set to "REAL"
  • Database migration applied in production
  • Monitoring/alerting configured in Sentry/Datadog
  • Logs aggregation setup for troubleshooting
  • Team trained on monitoring & troubleshooting
  • Rollback plan documented
  • On-call engineer identified for first week
  • Gradual rollout plan ready (blue-green or canary)

πŸ“ž Support & References


Document Version: 1.0
Last Updated: 2025-12-31
Next Review: 2025-01-15 (post-deployment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment