Skip to content

feat(kilo-pass): handle Play real-time notifications - #5598

Open
iscekic wants to merge 9 commits into
android-iap-e895-s3from
android-iap-e895-s4
Open

feat(kilo-pass): handle Play real-time notifications#5598
iscekic wants to merge 9 commits into
android-iap-e895-s3from
android-iap-e895-s4

Conversation

@iscekic

@iscekic iscekic commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Kilo Pass subscriptions bought on Google Play for Android now follow the store's subscription events.

  • A purchase now activates the subscription immediately.
  • A renewal, recovery, or restart now extends the subscription automatically.
  • A cancellation now keeps the subscription active until the paid period ends.
  • A refund or revocation now ends the subscription and removes the credits the purchase granted.

A new processor turns Google Play Real-time Developer Notifications into Kilo Pass lifecycle actions. It decodes the base64 Pub/Sub payload, rejects a package-name mismatch, and routes each notification type: purchase, renewal, recovery, and restart complete the store purchase; canceled marks end-of-period cancellation; expired ends the subscription; revoked reverses the granted credits and ends it; unknown types are claimed and ignored. Each event claims a kilo_pass_store_events row before it acts, so a delayed or redelivered message cannot apply a purchase or a reversal twice, and the stored payload token is redacted.

Files
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts — new processor. It base64-decodes the Pub/Sub payload, checks the package name, fetches and decodes the Play subscription purchase, and claims the kilo_pass_store_events row with a five-minute stale reclaim. It routes each notification type to its lifecycle action, reverses base, bonus, and promo credits on revocation, skips purchase completion when a processed revoked event already settled, and marks the event processed.

The notification endpoint authenticates each Pub/Sub push before it reaches the processor. It verifies the OIDC bearer token against the GOOGLE_PLAY_RTDN_PUSH_AUDIENCE audience and now also requires the token's verified email claim to equal GOOGLE_PLAY_RTDN_PUSH_SERVICE_ACCOUNT_EMAIL, so an arbitrary GCP service account cannot invoke the billing webhook. A missing, invalid, or mismatched token returns 401, a malformed body returns 400, a fresh in-flight duplicate returns 503 for Pub/Sub to retry, and a processing error returns 500 without leaking details.

Files
  • apps/web/src/app/api/kilo-pass/play/notifications/route.ts — new POST route. It parses the body shape, reads both environment values, verifies the bearer with OAuth2Client, checks the email and email_verified claims against the configured service account, delegates to the processor, and reports failures to Sentry.
  • ENVIRONMENT.md — catalogs the new GOOGLE_PLAY_RTDN_PUSH_SERVICE_ACCOUNT_EMAIL value.

trackKiloPassPurchaseCompleted now accepts a google_play channel next to app_store, and the property branch now splits on stripe. A Google Play completion therefore emits the same kilo_pass_purchase_completed event with the provider transaction, product, and environment properties.

Files
  • apps/web/src/lib/kilo-pass/posthog-tracking.ts — adds google_play to the store channel union and changes the property branch condition to compare against stripe.

Tests: 3 files changed — route.test.ts and google-play-notifications.test.ts added, posthog-tracking.test.ts updated.
Generated: none.


Verification

No manual test paths were run. No E2E report is attached for this level — a lower PR names its own change alone, and the tip PR carries the E2E report.

Visual Changes

Visual Changes: N/A

Reviewer Notes

  • before merge: set GOOGLE_PLAY_RTDN_PUSH_AUDIENCE to the public HTTPS URL of the notifications endpoint. Run pnpm web:env set GOOGLE_PLAY_RTDN_PUSH_AUDIENCE.
  • before merge: set GOOGLE_PLAY_RTDN_PUSH_SERVICE_ACCOUNT_EMAIL to the email of the Play service account that the Pub/Sub push uses. The route returns 401 for every notification until both values are set.
  • E2E: bot-e2e — runtime verification runs once on the tip PR (level 6) after all levels land. This level is reviewed on its own diff.

Stacked PRs — merge bottom to top. Each level shows only its own diff.

Runtime verification (E2E, user advocacy, simplify) runs on the tip PR over every level.
Every level keeps its own checks, its own bot review, and its own threads; each one is answered on its own PR.
Each level is its own deliverable: it builds and passes its own checks alone.
A finding on a level is repaired on that level, then carried upward with stack.sh forward.

  1. android-iap-e895feat(commerce): add Play Kilo Pass presentation flag #5581
  2. android-iap-e895-s2feat(kilo-pass): add Google Play publisher SDK #5582
  3. android-iap-e895-s3feat(kilo-pass): add Google Play purchase verifier #5586
  4. android-iap-e895-s4feat(kilo-pass): handle Play real-time notifications #5598 ← this PR
  5. android-iap-e895-s5feat(kilo-pass): add completePlayPurchase mutation #5608
  6. android-iap-e895-s6feat(mobile): offer Google Play Kilo Pass purchase #5618 (tip)

Comment thread apps/web/src/app/api/kilo-pass/play/notifications/route.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Play RTDN OIDC auth now verifies the configured service-account email claim; incremental review found no remaining issues.

Files Reviewed (3 files)
  • ENVIRONMENT.md
  • apps/web/src/app/api/kilo-pass/play/notifications/route.ts
  • apps/web/src/app/api/kilo-pass/play/notifications/route.test.ts
Previous Review Summaries (2 snapshots, latest commit 280d7cc)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 280d7cc)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/app/api/kilo-pass/play/notifications/route.ts 21 Pub/Sub OIDC auth does not verify the service-account email claim
Files Reviewed (6 files)
  • apps/web/src/app/api/kilo-pass/play/notifications/route.ts - 1 issue
  • apps/web/src/app/api/kilo-pass/play/notifications/route.test.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.test.ts
  • apps/web/src/lib/kilo-pass/posthog-tracking.ts
  • apps/web/src/lib/kilo-pass/posthog-tracking.test.ts

Fix these issues in Kilo Cloud

Previous review (commit b909b8c)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/app/api/kilo-pass/play/notifications/route.ts 21 Pub/Sub OIDC auth does not verify the service-account email claim
Files Reviewed (6 files)
  • apps/web/src/app/api/kilo-pass/play/notifications/route.ts - 1 issue
  • apps/web/src/app/api/kilo-pass/play/notifications/route.test.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.ts
  • apps/web/src/lib/kilo-pass/google-play-notifications.test.ts
  • apps/web/src/lib/kilo-pass/posthog-tracking.ts
  • apps/web/src/lib/kilo-pass/posthog-tracking.test.ts

Fix these issues in Kilo Cloud


Reviewed by grok-4.6 · Input: 78.7K · Output: 8K · Cached: 289.3K

Review guidance: REVIEW.md from base branch android-iap-e895-s3

@iscekic iscekic added the human-ready The PR is ready for human review. label Aug 27, 2026
Comment on lines +776 to +778
try {
reversal = await reverseGooglePlayRefundCredits(tx, purchaseToken, decoded.latestOrderId);
} catch (error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bot: A failed refund clawback is acknowledged as a successful revocation.

Suggested fix: Do not catch-and-continue around reverseGooglePlayRefundCredits. Let the error escape (or explicitly rethrow after reporting it) so this transaction rolls back and the route returns 500, allowing Pub/Sub to redeliver the event. Only write the successful refund audit and processed_at after the reversal completes. Add a test which makes reversal fail and asserts that no processed event/success audit is committed and a retry can perform the clawback.

Comment on lines +759 to +760
if (notificationType === GOOGLE_PLAY_NOTIFICATION_TYPE.SUBSCRIPTION_EXPIRED) {
await markGooglePlaySubscriptionEnded(db, purchaseToken);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bot: A stale expiry notification can cancel a subscription that has already renewed or recovered.

Suggested fix: Use eventTimeMillis to reject terminal events older than the newest applied lifecycle event for this purchase token, or reconcile terminal transitions with the verified current Play subscription state before updating. Apply the same protection to cancellation. Add tests that process a newer active renewal/recovery first and then a delayed cancellation/expiry, asserting the subscription remains active.

return Response.json({ error: 'Unauthorized' }, { status: 401 });
}

const body = GooglePlayNotificationBodySchema.safeParse(await request.json());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bot: Malformed request JSON falls through as a 500 instead of the intended 400 client error.

Suggested fix: Wrap await request.json() in a small parse-specific try/catch and return the same 400 response used for an invalid Pub/Sub envelope; keep processor failures in the outer 500 handler. Add a test using a request with an invalid JSON body.

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

Labels

human-ready The PR is ready for human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants