Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import type { SubscriptionPlan, SubscriptionStatus } from "@app/shared-types";
|
||||
import { resolvePlanFromProductId } from "./entitlements.js";
|
||||
|
||||
/**
|
||||
* Butiksverifiering (spec §47, Del 13). Backend verifierar ALLTID köpet mot
|
||||
* butiken – klientens kvitto är bara ett påstående (spec §61.14).
|
||||
*
|
||||
* PRODUKTIONSINTEGRATION (implementeras i fas 7, se docs/utvecklingsfaser.md):
|
||||
* - Apple: App Store Server API v2 – verifiera JWS-signaturen i signedTransaction
|
||||
* mot Apples publika nycklar, kontrollera bundleId + environment.
|
||||
* - Google: Play Developer API purchases.subscriptionsv2.get med service account.
|
||||
*
|
||||
* Dev/CI: APP_STORE_MODE=sandbox accepterar särskilt formaterade
|
||||
* sandbox-payloads så att hela flödet kan testas end-to-end utan butikskonton.
|
||||
*/
|
||||
|
||||
export interface VerifiedPurchase {
|
||||
provider: "apple" | "google";
|
||||
productId: string;
|
||||
plan: SubscriptionPlan;
|
||||
originalTransactionId: string;
|
||||
status: SubscriptionStatus;
|
||||
purchasedAt: Date;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
export type StoreVerificationResult =
|
||||
{ ok: true; purchase: VerifiedPurchase } | { ok: false; error: string };
|
||||
|
||||
export interface StoreVerifierConfig {
|
||||
mode: "production" | "sandbox";
|
||||
appleBundleId?: string | undefined;
|
||||
googlePackageName?: string | undefined;
|
||||
}
|
||||
|
||||
export class StoreVerifier {
|
||||
constructor(private readonly cfg: StoreVerifierConfig) {}
|
||||
|
||||
async verifyApple(signedTransaction: string): Promise<StoreVerificationResult> {
|
||||
if (this.cfg.mode === "sandbox") {
|
||||
return parseSandboxPayload("apple", signedTransaction);
|
||||
}
|
||||
// TODO(fas 7): App Store Server API v2 JWS-verifiering.
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
"Apple-verifiering i produktionsläge är inte konfigurerad ännu. " +
|
||||
"Kräver APPLE_ISSUER_ID/KEY_ID/PRIVATE_KEY (se docs/subscriptions.md).",
|
||||
};
|
||||
}
|
||||
|
||||
async verifyGoogle(
|
||||
packageName: string,
|
||||
productId: string,
|
||||
purchaseToken: string,
|
||||
): Promise<StoreVerificationResult> {
|
||||
if (this.cfg.mode === "sandbox") {
|
||||
return parseSandboxPayload("google", purchaseToken, productId);
|
||||
}
|
||||
if (this.cfg.googlePackageName && packageName !== this.cfg.googlePackageName) {
|
||||
return { ok: false, error: `Fel paketnamn: ${packageName}` };
|
||||
}
|
||||
// TODO(fas 7): Play Developer API purchases.subscriptionsv2.
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
"Google-verifiering i produktionsläge är inte konfigurerad ännu. " +
|
||||
"Kräver GOOGLE_SERVICE_ACCOUNT_JSON (se docs/subscriptions.md).",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sandbox-payload: JSON i klartext:
|
||||
* {"productId":"<iosBundleId>.family_monthly","originalTransactionId":"sandbox-1","expiresInDays":30}
|
||||
*/
|
||||
function parseSandboxPayload(
|
||||
provider: "apple" | "google",
|
||||
payload: string,
|
||||
fallbackProductId?: string,
|
||||
): StoreVerificationResult {
|
||||
try {
|
||||
const data = JSON.parse(payload) as {
|
||||
productId?: string;
|
||||
originalTransactionId?: string;
|
||||
expiresInDays?: number;
|
||||
};
|
||||
const productId = data.productId ?? fallbackProductId;
|
||||
if (!productId) return { ok: false, error: "sandbox-payload saknar productId" };
|
||||
const plan = resolvePlanFromProductId(productId);
|
||||
if (!plan || plan === "free") {
|
||||
return { ok: false, error: `Okänt productId: ${productId}` };
|
||||
}
|
||||
const now = new Date();
|
||||
return {
|
||||
ok: true,
|
||||
purchase: {
|
||||
provider,
|
||||
productId,
|
||||
plan,
|
||||
originalTransactionId: data.originalTransactionId ?? `sandbox-${Date.now()}`,
|
||||
status: "active",
|
||||
purchasedAt: now,
|
||||
expiresAt: new Date(now.getTime() + (data.expiresInDays ?? 30) * 86_400_000),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return { ok: false, error: "Ogiltig sandbox-payload (förväntar JSON)" };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user