feat(kilo-pass): integrate Churnkey for cancel flow and payment recovery#1799
Closed
feat(kilo-pass): integrate Churnkey for cancel flow and payment recovery#1799
Conversation
Replace the custom cancel/feedback modals with Churnkey's SDK for intelligent cancel flows (surveys, discount offers, A/B testing). Cancellation still goes through our kiloPass.cancelSubscription mutation via handleCancel callback, preserving subscription schedule release logic. Churnkey applies discounts directly on Stripe (safe — coupons don't interact with subscription schedules).
Contributor
Code Review SummaryStatus: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Other Observations (not in diff)Issues found in unchanged code that cannot receive inline comments:
Files Reviewed (2 files)
Fix these issues in Kilo Cloud Reviewed by gpt-5.4-20260305 · 1,147,688 tokens |
When NEXT_PUBLIC_CHURNKEY_APP_ID is set but the auth query or SDK script fails at runtime, the catch block now offers a window.confirm fallback so the user can still cancel their subscription.
…onfigured Dead code — the early-return branch with window.confirm was unreachable in practice. The catch block fallback still handles runtime failures.
…ription The resumePausedSubscription mutation was only updating Stripe and closing the pause event, but not updating the DB status. This caused the UI to show stale 'paused' status until the webhook arrived.
…ix useCallback deps Add isResumingSubscription guard to prevent concurrent resume calls, matching the pattern used by other action handlers. Also add missing onClose to handleOpenCancelFlow dependency array.
When Stripe sets pause_collection, it keeps the subscription status as 'active' until the current period ends. Our webhook handler was storing that 'active' status, so the UI never showed the subscription as paused even though a pause event was created. Now we eagerly set the DB status to 'paused' when pause_collection is present.
…f DB status Stripe keeps the subscription status as 'active' when pause_collection is first set — it only becomes 'paused' at the next billing cycle. The state query now checks for open pause events to determine if a subscription is paused, rather than relying on the DB status column. This keeps the DB in sync with Stripe's reported status while ensuring the UI reflects the user's intent immediately.
…f casting webhook payload Replace unsafe cast of webhook subscription object with an explicit stripe.subscriptions.retrieve() call to get pause_collection data.
…f string comparison Drizzle returns timestamps as "YYYY-MM-DD HH:MM:SS+00" (space separator) while dayjs generates "YYYY-MM-DDT00:00:00.000Z" (T separator). String comparison fails at date boundaries because ASCII space (32) < T (84), causing paused months to be excluded from the streak walk. Switch to millisecond-based numeric comparison via dayjs().valueOf().
Preparing to merge main by removing 0067_silky_dark_phoenix migration and restoring schema.ts to main's version. Will re-add pause_events table and regenerate after merge.
…tion # Conflicts: # .env.development.local.example
Regenerated migration as 0071 after merging main (which added 0067-0070).
Churnkey env vars are configured via Vercel, no need in dotfiles.
Regenerated migration as 0072 after merging main (which added 0071).
Broken merge from #1839 left conflict markers on main. Combined both sides: callback target from RSO/fluff-pan + gitlabProject validation.
…-session" This reverts commit d184abe.
jeanduplessis
approved these changes
Apr 2, 2026
Contributor
|
Due to the monorepo restructure you will need to recreate this PR on a new branch from main. Pass the prompt found at, https://github.com/Kilo-Org/cloud/blob/main/plans/monorepo-migration-prompt.md, to your coding agent while running in this branch. Please close this PR once done or if you don't plan to proceed with it. |
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replace the custom 2-step cancel/feedback modals with Churnkey's SDK for intelligent cancel flows (surveys, targeted discount offers, A/B testing) and failed payment recovery. Add full pause/resume subscription support for Churnkey's pause offers.
Hybrid approach: Churnkey handles the cancel flow UI and applies discount coupons directly on Stripe, but actual cancellation goes through our existing
kiloPass.cancelSubscriptiontRPC mutation via the SDK'shandleCancelcallback. This preserves the subscription schedule release logic — Stripe coupons don't interact with subscription schedules, so letting Churnkey apply them directly is safe.Churnkey integration
src/lib/churnkey/auth.ts— server-side HMAC-SHA256 auth hash for Churnkey SDK securitysrc/lib/churnkey/loader.ts— client-side SDK script loader withshowCancelFlow()and fallbackkiloPass.getChurnkeyAuthHashtRPC query — returns auth hash + Stripe customer IDKiloPassSubscriptionSettingsModalto open Churnkey flowCancelKiloPassSubscriptionModal.tsxandKiloPassCancellationFeedbackModal.tsxwindow.confirm+ direct cancellation if Churnkey SDK fails to loadNEXT_PUBLIC_CHURNKEY_APP_IDandCHURNKEY_API_SECRETenv varsPause/resume subscription support
kilo_pass_pause_eventstable with partial unique index (one open pause per sub) and CHECK constraint (resumed_at >= paused_at)openPauseEvent,closePauseEvent,getOpenPauseEvent,getPausedMonthSet— all idempotentpause_collectiontransitions in subscription webhook handler (open/close pause events)kiloPass.resumePausedSubscriptionmutation — clearspause_collectionon Stripe, updates DB status toactive, closes pause eventresumesAtdate and resume buttonresumeSubscription→resumeCancelledSubscriptionto distinguish from pause resumeWhat doesn't change
cancelSubscriptionmutation, scheduled changes system, all other Stripe webhook handlers, credit issuance logic, mobile app.Verification
pnpm run typecheck— passedpnpm run lint— passed (0 warnings, 0 errors)pnpm run format:changed— passedpnpm run test— passed (pre-existing kiloclaw-billing-router timeouts unrelated)Reviewer Notes
NEXT_PUBLIC_CHURNKEY_APP_IDandCHURNKEY_API_SECRETmust be set. Without them, the cancel flow falls back towindow.confirm+ direct mutation.handleCancelcalls our mutation which releases the schedule before cancelling.