Phase 3.4: customers.display_name nullable + identity-only social scope

Drop NOT NULL on customers.display_name so phone-OTP and social signups can
land before the user picks a name; frontend then routes them to /auth/set-name.
Google sign-in no longer requests the name claim and Apple SDK scope is
trimmed to email only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 13:43:37 +08:00
parent 6801001b64
commit 6de541848c
3 changed files with 10 additions and 4 deletions

View File

@@ -352,6 +352,10 @@ const migrate = async () => {
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_google_sub ON customers (google_sub) WHERE google_sub IS NOT NULL` await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_google_sub ON customers (google_sub) WHERE google_sub IS NOT NULL`
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_apple_sub ON customers (apple_sub) WHERE apple_sub IS NOT NULL` await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_customers_apple_sub ON customers (apple_sub) WHERE apple_sub IS NOT NULL`
// display_name is set after sign-in via the set-display-name screen for
// direct phone/Google/Apple sign-ups (no anonymous bootstrap). Allow null.
await sql`ALTER TABLE customers ALTER COLUMN display_name DROP NOT NULL`
// Control center users: password-based auth columns // Control center users: password-based auth columns
// firebase_uid stays for backward compat during migration; will be dropped in a later cleanup migration // firebase_uid stays for backward compat during migration; will be dropped in a later cleanup migration
await sql`ALTER TABLE control_center_users ALTER COLUMN firebase_uid DROP NOT NULL` await sql`ALTER TABLE control_center_users ALTER COLUMN firebase_uid DROP NOT NULL`

View File

@@ -112,16 +112,18 @@ export const signInWithGoogle = async ({ idToken, anonymousCustomerId, deviceInf
if (existing) { if (existing) {
customer = existing customer = existing
} else if (anonymousCustomerId) { } else if (anonymousCustomerId) {
// Preserve the anonymous display_name; we don't pull name from Google.
customer = await upgradeCustomerIdentity(anonymousCustomerId, { customer = await upgradeCustomerIdentity(anonymousCustomerId, {
google_sub: google.sub, google_sub: google.sub,
email: google.email, email: google.email,
display_name: google.name,
}) })
} else { } else {
// No anonymous bootstrap → display_name is null; frontend routes to
// the set-display-name screen.
customer = await createCustomerWithIdentity({ customer = await createCustomerWithIdentity({
google_sub: google.sub, google_sub: google.sub,
email: google.email, email: google.email,
display_name: google.name, display_name: null,
}) })
} }

View File

@@ -11,7 +11,8 @@ const googleClient = new OAuth2Client()
/** /**
* Verify a Google ID token against Google's JWKS. * Verify a Google ID token against Google's JWKS.
* Throws on invalid; returns { sub, email, email_verified, name } on success. * Throws on invalid; returns { sub, email, email_verified } on success.
* Intentionally omits the user's name — call sign is set in-app.
*/ */
export const verifyGoogleIdToken = async (idToken) => { export const verifyGoogleIdToken = async (idToken) => {
const audience = getGoogleClientIds() const audience = getGoogleClientIds()
@@ -30,7 +31,6 @@ export const verifyGoogleIdToken = async (idToken) => {
sub: payload.sub, sub: payload.sub,
email: payload.email, email: payload.email,
email_verified: payload.email_verified === true, email_verified: payload.email_verified === true,
name: payload.name,
} }
} catch (err) { } catch (err) {
throw Object.assign(new Error(err.message || 'Invalid Google token'), { throw Object.assign(new Error(err.message || 'Invalid Google token'), {