5 min read

Retrieve Active In-App Purchases with a React Component: A Step-by-Step Guide

Retrieve Active In-App Purchases with a React Component: A Step-by-Step Guide

One of the goals of my blog is to share all my learnings as an app maker. I want to share with you all the steps I take to grow my apps and the strategies I use to increase my revenue. But before I can do that, I need to display the current InApp purchases that are active in my apps.

So, I wanted to display my current InApp Purchases on the homepage of my blog, and to do that, I need to use the App Store Connect API. To call the Apple APIs, we need to generate an API key and use it to authenticate the requests.

Generate the App Store API Token

First, we will need to access our App Store Connect account and get the issuer ID and the key ID. Then we will need to generate a private key and save it in a file. You can download the private key from the App Store Connect website and add it into your project.

Here’s a brief summary of what we need:

  • keyId
  • issuerId
  • privateKey (.p8 file)

After we have all the information we need, we can create a function to generate the token (take into account that you will need to install the jsonwebtoken package).

import fs from 'fs';
import jwt from 'jsonwebtoken';
import path from 'path';
const privateKeyPath = ""
const keyId = ""
const issuerId = ""
const absolutePath = path.resolve(privateKeyPath);
if (!fs.existsSync(absolutePath)) {
throw new Error(`Private key file not found at path: ${absolutePath}`);
}
const privateKey = fs.readFileSync(absolutePath, 'utf8');
export function generateToken() {
const now = Math.floor(Date.now() / 1000);
const expirationTime = now + 20 * 60;
const token = jwt.sign(
{
iss: issuerId,
iat: now,
exp: expirationTime,
aud: "appstoreconnect-v1"
},
privateKey,
{
algorithm: 'ES256',
header: {
alg: 'ES256',
kid: keyId
}
}
);
return token;
}

Once we have created the token, we can start making requests to the App Store Connect API. Which endpoint do we need to call?

Step-by-Step to Obtain InApp Purchases from App Store Connect

We will use the GET /v1/salesReports endpoint to retrieve the active InApp Purchases. We will need to pass some parameters to extract this information. As we did before, we will need another value to send in our request that it’s in the App Store Connect, this values is our vendorNumber (take into account that you will need to install the zlib package).

In this case, we will create an API endpoint that will return the total number of active InApp Purchases. This endpoint will be called from one of our components (Spoiler: we will create this component in a minute).

import { generateToken } from '../../lib/create_apple_token.js';
import zlib from 'zlib';
export const GET = async () => {
const total = await fetchSalesReport();
return new Response(JSON.stringify({ total }), {
status: 200,
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store, max-age=0",
"CDN-Cache-Control": "no-store",
"Vercel-CDN-Cache-Control": "no-store"
}
});
}
async function fetchSalesReport(maxRetries = 3, initialDelay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const token = await generateToken();
const apiEndpoint = 'https://api.appstoreconnect.apple.com/v1/salesReports';
const today = new Date();
const twoDaysAgo = new Date(today.setDate(today.getDate() - 2));
const formattedDate = twoDaysAgo.toISOString().split('T')[0];
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip'
};
const params = new URLSearchParams({
'filter[frequency]': 'DAILY',
'filter[reportType]': 'SUBSCRIPTION',
'filter[reportSubType]': 'SUMMARY',
'filter[reportDate]': formattedDate,
'filter[vendorNumber]': '85204056',
'filter[version]': '1_3'
});
const response = await fetch(`${apiEndpoint}?${params}`, { headers });
if (!response.ok) {
const errorBody = await response.text();
const errorInfo = {
status: response.status,
statusText: response.statusText,
body: errorBody
};
if (response.status === 429) {
console.error('Rate limit error detected:', errorInfo);
} else if (response.status === 500) {
console.error('Server error detected:', errorInfo);
console.log('This may be a temporary issue with Apple\'s servers.');
return "Error from 🍏";
} else {
console.error('Other error:', errorInfo);
}
throw new Error(`HTTP error! Details: ${JSON.stringify(errorInfo)}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const decompressed = await decompressGzip(buffer);
const decompressedString = decompressed.toString();
const lines = decompressedString.split('\n');
//console.log('CSV content:');
//console.log(decompressedString);
// Parse CSV to count active subscriptions
const activeSubscriptions = countActiveSubscriptions(lines);
console.log(`Number of active subscriptions: ${activeSubscriptions}`);
console.log(`NĂşmero de suscripciones activas: ${activeSubscriptions}`);
return activeSubscriptions;
} catch (error) {
if (error instanceof Error) {
console.error(`Attempt ${attempt} failed:`, error.message);
console.error(`Intento ${attempt} fallĂł:`, error.message);
} else {
console.error(`Attempt ${attempt} failed:`, String(error));
console.error(`Intento ${attempt} fallĂł:`, String(error));
}
if (attempt === maxRetries) {
console.error('All retry attempts failed');
console.log('If this issue persists, please contact Apple support at https://developer.apple.com/contact/');
return "Error: Unable to fetch sales report after multiple attempts. Please try again later.";
}
const delay = initialDelay * Math.pow(2, attempt - 1);
console.log(`Waiting ${delay}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
function countActiveSubscriptions(lines: string[]): number {
let activeCount = 0;
const headers = lines[0].split('\t');
//console.log('CSV headers:', headers);
const activeSubscriptionsIndex = headers.indexOf('Active Standard Price Subscriptions');
if (activeSubscriptionsIndex === -1) {
console.error('Column "Active Standard Price Subscriptions" not found in the CSV');
return 0;
}
for (let i = 1; i < lines.length; i++) {
const columns = lines[i].split('\t');
const activeSubscriptions = parseInt(columns[activeSubscriptionsIndex], 10) || 0;
activeCount += activeSubscriptions;
//console.log(`Line ${i}: Active subscriptions = ${activeSubscriptions}`);
}
//console.log(`Total active subscriptions: ${activeCount}`);
return activeCount;
}
function decompressGzip(data: Buffer): Promise<Buffer> {
return new Promise((resolve, reject) => {
zlib.gunzip(data, (err, decompressed) => {
if (err) reject(err);
else resolve(decompressed);
});
});
}

Now, we have the function that will retrieve the total number of active InApp Purchases. So, we need to create a React Component that will display this information on our homepage.

Create the React Component

We will connect all the dots and create a React component that will display the total number of active InApp Purchases. So, we will use the useEffect hook to fetch the data from our API endpoint. After receiving the number of InApp Purchases we will display it in a simple div.

import { useState, useEffect } from 'react';
export default function CounterIAP() {
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/salesReport.json');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
setTotal(data.total);
setLoading(false);
} catch (error) {
console.error('Error fetching sales report:', error);
setError('Failed to load data');
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>{error}</div>;
return (
<div>
Active IAPs #{total}
</div>
);
}

That’s it! Now we have a component that will display the total number of active InApp Purchases. If you have an alternative method, please share it with me on X (formerly called Twitter).