Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .infra/Pulumi.prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ config:
apiConfigFeatureKey: api-config
appleAppAppleId: 6740634400
appleAppBundleId: dev.daily.app
appleIssuerId:
secure: AAABAK7J9W93y2dJnNDSiDfzwj2+bSPdo3uOswwQE/vX4RrWSVtPndgGY4EStNzK5dAyV34Osd5Ktekn9wHyEeptNN4=
appleAppStoreServerClientKey:
secure: AAABAPaOTw3J1adzb3RaGgZYE7eywB9UHAw85tagbYBXaIzM6MxQI7vgSFUP5bbvMwuZVaZ2F2aB5WXn0V6ZcoqV/OCGJk/XygbjovFqMlqCEjO4ovRu0Cjck+82nJYk6hPuLqsTUw6C0aJUBour4a1o2Jl04X5XRcUUTTanguOyFlWxSUxVDZ6As9DGuFYsNV3jIhhiAtgC1IHZH/luyURuUkprm7iHKvdlVqMO9ctne+PQ2bMVWCSGvJeXt/oQDWRQT9UHxnbD7LSgWDmMAbd4aaXD/YE/gN220FSzeV7EsiXUb4+NyPtP6dSc4GSKVfa7keGn57KZK+U+duFZjhEqch8uenb0RO28zvyYYcbbcpsd7TvggwbBgqc2cTJXUA==
appleAppStoreServerClientKeyId:
secure: AAABAOxbRgC+Npj/q0uRCmHXjAnMs8W507CAyKb50ndVgQkx1YKyYPZb
validLanguagesFeatureKey: valid_languages
cdcWorkerMaxMessages: 1
cioApiKey:
Expand Down
79 changes: 78 additions & 1 deletion src/common/apple/purchase.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
import {
AppStoreServerAPIClient,
ConsumptionStatus,
DeliveryStatus,
Environment,
JWSTransactionDecodedPayload,
LifetimeDollarsPurchased,
LifetimeDollarsRefunded,
Platform,
PlayTime,
ResponseBodyV2DecodedPayload,
UserStatus,
type ConsumptionRequest,
} from '@apple/app-store-server-library';
import { RefundPreference } from '@apple/app-store-server-library/dist/models/RefundPreference';
import { JsonContains } from 'typeorm';
import type { User } from '../../entity/user/User';
import {
getAccountTenure,
getAnalyticsEventFromAppleNotification,
getAppleTransactionType,
logAppleAnalyticsEvent,
} from './utils';
import { AppleTransactionType } from './types';
import {
appleAppStoreServerClientKey,
appleAppStoreServerClientKeyId,
appleIssuerId,
AppleTransactionType,
bundleId,
} from './types';
import {
UserTransaction,
UserTransactionProcessor,
Expand Down Expand Up @@ -281,3 +299,62 @@ export const handleCoresPurchase = async ({

return transaction;
};

export const handleCoresConsumptionRequest = async ({
transactionInfo,
user,
environment,
}: {
transactionInfo: JWSTransactionDecodedPayload;
user: Pick<User, 'id' | 'subscriptionFlags' | 'coresRole' | 'createdAt'>;
environment: Environment;
}) => {
if (!transactionInfo.transactionId) {
throw new Error('Missing transactionId in transactionInfo');
}

if (!user.subscriptionFlags?.appAccountToken) {
throw new Error('Missing appAccountToken in user subscription flags');
}

const apiClient = new AppStoreServerAPIClient(
appleAppStoreServerClientKey,
appleAppStoreServerClientKeyId,
appleIssuerId,
bundleId,
environment,
);

const con = await createOrGetConnection();

const deliveryStatus = await con.getRepository(UserTransaction).exists({
where: {
receiverId: user.id,
flags: JsonContains({ providerId: transactionInfo.transactionId }),
},
});

const consumptionRequest: ConsumptionRequest = {
accountTenure: getAccountTenure(user),
appAccountToken: user.subscriptionFlags.appAccountToken,
consumptionStatus: ConsumptionStatus.FULLY_CONSUMED, // It is assumed that the user has fully consumed the in-app purchase
customerConsented: true,
deliveryStatus: deliveryStatus
? DeliveryStatus.DELIVERED_AND_WORKING_PROPERLY
: DeliveryStatus.DID_NOT_DELIVER_DUE_TO_SERVER_OUTAGE,
refundPreference: RefundPreference.PREFER_DECLINE,
userStatus: UserStatus.ACTIVE,

// Values we don't track or care about, but are required by Apple
sampleContentProvided: false,
lifetimeDollarsPurchased: LifetimeDollarsPurchased.UNDECLARED,
lifetimeDollarsRefunded: LifetimeDollarsRefunded.UNDECLARED,
platform: Platform.UNDECLARED,
playTime: PlayTime.UNDECLARED,
};

await apiClient.sendConsumptionData(
transactionInfo.transactionId,
consumptionRequest,
);
};
5 changes: 5 additions & 0 deletions src/common/apple/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export const bundleId = isTest ? 'dev.fylla' : env.APPLE_APP_BUNDLE_ID;
export const appAppleId = parseInt(env.APPLE_APP_APPLE_ID);
export const appleEnableOnlineChecks = true;
export const appleEnvironment = getVerifierEnvironment();
export const appleIssuerId = env.APPLE_ISSUER_ID;
export const appleAppStoreServerClientKey =
env.APPLE_APP_STORE_SERVER_CLIENT_KEY;
export const appleAppStoreServerClientKeyId =
env.APPLE_APP_STORE_SERVER_CLIENT_KEY_ID;

export const allowedIPs = [
'127.0.0.1/24',
Expand Down
20 changes: 20 additions & 0 deletions src/common/apple/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AccountTenure,
NotificationTypeV2,
Subtype,
type Environment,
Expand All @@ -24,6 +25,7 @@ import { readFile } from 'fs/promises';
import { isTest } from '../utils';
import { isCorePurchaseApple } from './purchase';
import { SubscriptionProvider } from '../plus';
import { differenceInDays } from 'date-fns';

export const verifyAndDecodeAppleSignedData = async ({
notification,
Expand Down Expand Up @@ -155,3 +157,21 @@ export const getAppleTransactionType = ({
return null;
}
};

export const getAccountTenure = (user: Pick<User, 'createdAt'>): number => {
if (!user.createdAt) {
return 0;
}

const difference = differenceInDays(new Date(), user.createdAt);

if (difference < 3) return AccountTenure.ZERO_TO_THREE_DAYS;
if (difference < 10) return AccountTenure.THREE_DAYS_TO_TEN_DAYS;
if (difference < 30) return AccountTenure.TEN_DAYS_TO_THIRTY_DAYS;
if (difference < 90) return AccountTenure.THIRTY_DAYS_TO_NINETY_DAYS;
if (difference < 180)
return AccountTenure.NINETY_DAYS_TO_ONE_HUNDRED_EIGHTY_DAYS;
if (difference < 365)
return AccountTenure.ONE_HUNDRED_EIGHTY_DAYS_TO_THREE_HUNDRED_SIXTY_FIVE_DAYS;
return AccountTenure.GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS;
};
50 changes: 35 additions & 15 deletions src/routes/webhooks/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
AppleTransactionType,
} from '../../common/apple/types';
import {
handleCoresConsumptionRequest,
handleCoresPurchase,
isCorePurchaseApple,
} from '../../common/apple/purchase';
Expand Down Expand Up @@ -82,15 +83,17 @@ const handleNotifcationRequest = async (
}

const con = await createOrGetConnection();
const user: Pick<User, 'id' | 'subscriptionFlags' | 'coresRole'> | null =
await con.getRepository(User).findOne({
select: ['id', 'subscriptionFlags', 'coresRole'],
where: {
subscriptionFlags: JsonContains({
appAccountToken: transactionInfo.appAccountToken,
}),
},
});
const user: Pick<
User,
'id' | 'subscriptionFlags' | 'coresRole' | 'createdAt'
> | null = await con.getRepository(User).findOne({
select: ['id', 'subscriptionFlags', 'coresRole', 'createdAt'],
where: {
subscriptionFlags: JsonContains({
appAccountToken: transactionInfo.appAccountToken,
}),
},
});

if (!user) {
logger.error(
Expand Down Expand Up @@ -124,12 +127,29 @@ const handleNotifcationRequest = async (
switch (getAppleTransactionType({ transactionInfo })) {
case AppleTransactionType.Consumable:
if (isCorePurchaseApple({ transactionInfo })) {
await handleCoresPurchase({
transactionInfo,
user,
environment,
notification,
});
switch (notification.notificationType) {
case NotificationTypeV2.ONE_TIME_CHARGE:
await handleCoresPurchase({
transactionInfo,
user,
environment,
notification,
});
break;
case NotificationTypeV2.CONSUMPTION_REQUEST:
await handleCoresConsumptionRequest({
transactionInfo,
user,
environment,
});
break;
case NotificationTypeV2.REFUND_DECLINED:
// No action needed for refund declined on consumables
break;
case NotificationTypeV2.REFUND: // TODO: Handle refunds for consumables if needed - since it is Apple who decides if a refund is given, we may need to revoke the consumable
default:
throw new Error('Unsupported Apple Consumable notification type');
}
} else {
throw new Error('Unsupported Apple Consumable transaction type');
}
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ declare global {

APPLE_APP_APPLE_ID: string;
APPLE_APP_BUNDLE_ID: string;
APPLE_ISSUER_ID: string;
APPLE_APP_STORE_SERVER_CLIENT_KEY: string;
APPLE_APP_STORE_SERVER_CLIENT_KEY_ID: string;

GEOIP_PATH?: string;
RESUME_BUCKET_NAME: string;
Expand Down
Loading