feat(auth): grant initial trial only after BOTH email verified AND name saved (BUNYIP-221) #250

Merged
YousifShkara merged 1 commit from feat/BUNYIP-221-trial-after-onboarding into main 2026-06-26 08:28:27 +02:00
Owner

Post-BUNYIP-206 the onboarding gate kept users on /onboarding until first + last name were saved AND the email was verified, but the trial / tier grant in AuthService::confirm_email_verification still fired the instant the verify link was clicked, regardless of whether names had been entered. A user could verify, never type a name, and sit on the onboarding page burning trial days, and hostile signups could claim the slot-limited Lifetime / EarlyAdopter tiers without ever completing onboarding.

Move the grant behind both conditions via a new AuthService::maybe_grant_initial_tier(user_id, ip, trigger). The helper re-reads the user, gates on email_verified + non-empty first + non-empty last, idempotent-skips already-granted users (lifetime_member, trial_ends_at populated, or non-default subscription_tier), then runs the existing advisory-locked tier-assign tx with a FOR UPDATE re-read so two concurrent triggers cannot double-grant. Both endpoints call it: confirm_email_verification (after the verify commits) and update_current_user_profile (after a successful name save), so whichever side closes the gate produces the grant. The lifetime $0-subscription side-effect is lifted into a shared maybe_create_lifetime_subscription helper so both call sites stay in sync.

AuditAction gains InitialTierGranted; EmailVerified is logged separately for the verify itself with no tier metadata. confirm_email_verification's return type changes from (Uuid, String, SubscriptionTier) to (Uuid, String, Option); the bunyip-web wrapper already uses unwrap_or_default, so an empty subscription_tier in the response body falls through to a neutral "verified" message on the page.

#BUNYIP-221

Post-BUNYIP-206 the onboarding gate kept users on /onboarding until first + last name were saved AND the email was verified, but the trial / tier grant in AuthService::confirm_email_verification still fired the instant the verify link was clicked, regardless of whether names had been entered. A user could verify, never type a name, and sit on the onboarding page burning trial days, and hostile signups could claim the slot-limited Lifetime / EarlyAdopter tiers without ever completing onboarding. Move the grant behind both conditions via a new AuthService::maybe_grant_initial_tier(user_id, ip, trigger). The helper re-reads the user, gates on email_verified + non-empty first + non-empty last, idempotent-skips already-granted users (lifetime_member, trial_ends_at populated, or non-default subscription_tier), then runs the existing advisory-locked tier-assign tx with a FOR UPDATE re-read so two concurrent triggers cannot double-grant. Both endpoints call it: confirm_email_verification (after the verify commits) and update_current_user_profile (after a successful name save), so whichever side closes the gate produces the grant. The lifetime $0-subscription side-effect is lifted into a shared maybe_create_lifetime_subscription helper so both call sites stay in sync. AuditAction gains InitialTierGranted; EmailVerified is logged separately for the verify itself with no tier metadata. confirm_email_verification's return type changes from (Uuid, String, SubscriptionTier) to (Uuid, String, Option<SubscriptionTier>); the bunyip-web wrapper already uses unwrap_or_default, so an empty subscription_tier in the response body falls through to a neutral "verified" message on the page. #BUNYIP-221
feat(auth): grant initial trial only after BOTH email verified AND name saved (BUNYIP-221)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 23s
Check / fmt + clippy + build + tests (pull_request) Successful in 10m39s
Create release / Create release from merged PR (pull_request) Has been skipped
f3136baae0
Post-BUNYIP-206 the onboarding gate kept users on /onboarding until first + last name were saved AND the email was verified, but the trial / tier grant in AuthService::confirm_email_verification still fired the instant the verify link was clicked, regardless of whether names had been entered. A user could verify, never type a name, and sit on the onboarding page burning trial days, and hostile signups could claim the slot-limited Lifetime / EarlyAdopter tiers without ever completing onboarding.

Move the grant behind both conditions via a new AuthService::maybe_grant_initial_tier(user_id, ip, trigger). The helper re-reads the user, gates on email_verified + non-empty first + non-empty last, idempotent-skips already-granted users (lifetime_member, trial_ends_at populated, or non-default subscription_tier), then runs the existing advisory-locked tier-assign tx with a FOR UPDATE re-read so two concurrent triggers cannot double-grant. Both endpoints call it: confirm_email_verification (after the verify commits) and update_current_user_profile (after a successful name save), so whichever side closes the gate produces the grant. The lifetime $0-subscription side-effect is lifted into a shared maybe_create_lifetime_subscription helper so both call sites stay in sync.

AuditAction gains InitialTierGranted; EmailVerified is logged separately for the verify itself with no tier metadata. confirm_email_verification's return type changes from (Uuid, String, SubscriptionTier) to (Uuid, String, Option<SubscriptionTier>); the bunyip-web wrapper already uses unwrap_or_default, so an empty subscription_tier in the response body falls through to a neutral "verified" message on the page.

#BUNYIP-221
YousifShkara deleted branch feat/BUNYIP-221-trial-after-onboarding 2026-06-26 08:28:27 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!250
No description provided.