diff --git a/_database/sql/migrations_squashed/user_tables/users.sql b/_database/sql/migrations_squashed/user_tables/users.sql index fc78d486d5..7cfbfeb39e 100644 --- a/_database/sql/migrations_squashed/user_tables/users.sql +++ b/_database/sql/migrations_squashed/user_tables/users.sql @@ -207,17 +207,17 @@ CREATE TABLE users ( maybe_source VARCHAR(255) DEFAULT NULL, -- How users created their account - -- Initially this will be "email_password" or "google_sign_in" - -- Older users do not have a value and are assumed to be "email_sign_in" + -- Older users do not have a value and are assumed to be "email_password" -- Values: - -- - "email_sign_in" / "email_password" (todo: check which is correct) + -- - "email_password" -- - "google_sign_in" -- - "stripe_checkout" (eager stripe checkout account provisioning) maybe_signup_method VARCHAR(16) DEFAULT NULL, -- Account was provisioned automatically (eg. stripe eager checkout flow) -- This will remain true even if the user finishes account setup, email change, - -- password setup, etc. + -- password setup, etc. (It's a permanent label on the account for later + -- statistical evaluation.) was_eagerly_provisioned BOOLEAN NOT NULL DEFAULT false, -- ========== MODERATION DETAILS ========== diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_email_and_password.rs b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_email_and_password.rs index b882ebf0ce..8e99bbc6d6 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_email_and_password.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_email_and_password.rs @@ -64,6 +64,11 @@ pub async fn create_account_from_email_and_password( // NB: This is just for testing. maybe_user_token: args.maybe_user_token, + + // These flags represent eagerly created users and are false for this flow + is_temporary: false, + email_is_synthetic: false, + was_eagerly_provisioned: false, }, Transactor::for_pool(mysql_pool), ).await?; diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_google_sso.rs b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_google_sso.rs index 041c617e19..f1d9938663 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_google_sso.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_google_sso.rs @@ -59,6 +59,11 @@ pub async fn create_account_from_google_sso( // NB: This is just for testing. maybe_user_token: None, + + // These flags represent eagerly created users and are false for this flow + is_temporary: false, + email_is_synthetic: false, + was_eagerly_provisioned: false, }, transactor, ).await?; diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_stripe_checkout.rs b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_stripe_checkout.rs index dc167c8fe3..a0ab64f334 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_stripe_checkout.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_from_stripe_checkout.rs @@ -16,6 +16,14 @@ use tokens::tokens::users::UserToken; const SSO_PASSWORD : &str = "*"; pub struct CreateAccountFromGoogleSsoArgs<'a> { + // NB: Username will likely be generated by our code, not the user. + pub username: &'a str, + pub display_name: &'a str, + + // NB: Email will likely be generated by our code, not the user. + pub email_address: &'a str, + pub email_gravatar_hash: &'a str, + pub ip_address: &'a str, pub maybe_source: Option, @@ -30,23 +38,32 @@ pub async fn create_account_from_google_sso( { let result= create_account_generic( GenericCreateAccountArgs { - maybe_signup_method: Some(UserSignupMethod::GoogleSignIn), + maybe_signup_method: Some(UserSignupMethod::StripeCheckout), - username: args.username, - display_name: args.display_name, ip_address: args.ip_address, maybe_feature_flags: args.maybe_feature_flags, maybe_source: args.maybe_source, - // SSO accounts have an email reported from Google - email_address: args.email_address, - email_gravatar_hash: args.email_gravatar_hash, - email_confirmed_by_google: args.email_confirmed_by_google, + // These are synthetic user accounts that are being eagerly created + // If the user abandons the onboarding flow, they can be deleted later. + is_temporary: true, + was_eagerly_provisioned: true, - // User-created accounts in this flow have randomly generated usernames + // Stripe checkout users have randomly generated usernames + username: args.username, + display_name: args.display_name, username_is_generated: true, username_is_not_customized: true, + // Stripe checkout users have NO REAL EMAIL ADDRESS yet. + // Stripe will tell us what their email address is once + // they complete the checkout (or the user can update it, + // and their write wins.) + email_address: args.email_address, + email_gravatar_hash: args.email_gravatar_hash, + email_confirmed_by_google: false, + email_is_synthetic: true, + // SSO accounts start without a password password_hash: SSO_PASSWORD, is_without_password: true, diff --git a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_generic.rs b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_generic.rs index 9e3aac025d..a660cd6023 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_generic.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user/create/create_account_generic.rs @@ -23,10 +23,35 @@ pub struct GenericCreateAccountArgs<'a> { pub email_address: &'a str, pub email_gravatar_hash: &'a str, + + /// Google did email IDV and told us it's legitimate. pub email_confirmed_by_google: bool, + + /// The email address is not real yet. We generated a fake/synthetic value + /// that will hopefully be replaced later. This is for automated user creation + /// flows, like Stripe Checkout onboarding flow. + pub email_is_synthetic: bool, pub password_hash: &'a str, pub is_without_password: bool, + + /// This is initially TRUE for accounts that were provisioned automatically, + /// without user intervention, e.g. in the eager Stripe Checkout onboarding + /// flow. Accounts in this state have not yet been customized by users and may + /// not in fact represent real users if the users abandon the flow. Once the + /// user finishes setup, this will be set to false. + /// + /// Existing and normal setup flow sets this flag to false. + /// + /// If enough time has passed without the user interacting with the account, + /// then it might be prudent to delete these records. + pub is_temporary: bool, + + /// Whether the account was created eagerly (e.g. via stripe checkout flow) + /// without the user explicitly setting up an account, choosing an email or + /// password, etc. (It's a technical choice to do faster checkout flow.) + /// This is a permanent label on the account. + pub was_eagerly_provisioned: bool, pub ip_address: &'a str, pub maybe_source: Option, @@ -66,6 +91,9 @@ pub async fn create_account_generic( INSERT INTO users SET token = ?, + + is_temporary = ?, + username = ?, display_name = ?, @@ -77,6 +105,9 @@ SET email_confirmed = FALSE, email_confirmed_by_google = ?, + email_is_synthetic = ?, + + was_eagerly_provisioned = ?, profile_markdown = ?, profile_rendered_html = ?, @@ -96,6 +127,9 @@ SET maybe_signup_method = ? "#, user_token.as_str(), + + args.is_temporary, + args.username, args.display_name, @@ -106,6 +140,9 @@ SET args.email_gravatar_hash, args.email_confirmed_by_google, + args.email_is_synthetic, + + args.was_eagerly_provisioned, INITIAL_PROFILE_MARKDOWN, INITIAL_PROFILE_RENDERED_HTML, diff --git a/crates/schema/public/enums/src/by_table/users/user_signup_method.rs b/crates/schema/public/enums/src/by_table/users/user_signup_method.rs index a5be1c6ce9..57e62ed6b5 100644 --- a/crates/schema/public/enums/src/by_table/users/user_signup_method.rs +++ b/crates/schema/public/enums/src/by_table/users/user_signup_method.rs @@ -18,6 +18,11 @@ pub enum UserSignupMethod { /// "Sign in With Google" SSO GoogleSignIn, + + /// Stripe Checkout flow, where we provision user accounts for users with a + /// synthetic/fake email address and no password. After checkout completes, + /// the user gets a real email and password - or the user can set them. + StripeCheckout, } // TODO(bt, 2022-12-21): This desperately needs MySQL integration tests! @@ -31,6 +36,7 @@ impl UserSignupMethod { match self { Self::EmailPassword => "email_password", Self::GoogleSignIn=> "google_sign_in", + Self::StripeCheckout => "stripe_checkout", } } @@ -38,6 +44,7 @@ impl UserSignupMethod { match value { "email_password" => Ok(Self::EmailPassword), "google_sign_in" => Ok(Self::GoogleSignIn), + "stripe_checkout" => Ok(Self::StripeCheckout), _ => Err(format!("invalid value: {:?}", value)), } } @@ -48,6 +55,7 @@ impl UserSignupMethod { BTreeSet::from([ Self::EmailPassword, Self::GoogleSignIn, + Self::StripeCheckout, ]) } } @@ -64,6 +72,7 @@ mod tests { fn test_serialization() { assert_serialization(UserSignupMethod::EmailPassword, "email_password"); assert_serialization(UserSignupMethod::GoogleSignIn, "google_sign_in"); + assert_serialization(UserSignupMethod::StripeCheckout, "stripe_checkout"); } } @@ -74,12 +83,14 @@ mod tests { fn to_str() { assert_eq!(UserSignupMethod::EmailPassword.to_str(), "email_password"); assert_eq!(UserSignupMethod::GoogleSignIn.to_str(), "google_sign_in"); + assert_eq!(UserSignupMethod::StripeCheckout.to_str(), "stripe_checkout"); } #[test] fn from_str() { assert_eq!(UserSignupMethod::from_str("email_password").unwrap(), UserSignupMethod::EmailPassword); assert_eq!(UserSignupMethod::from_str("google_sign_in").unwrap(), UserSignupMethod::GoogleSignIn); + assert_eq!(UserSignupMethod::from_str("stripe_checkout").unwrap(), UserSignupMethod::StripeCheckout); assert!(UserSignupMethod::from_str("foo").is_err()); } } @@ -90,9 +101,10 @@ mod tests { #[test] fn all_variants() { let mut variants = UserSignupMethod::all_variants(); - assert_eq!(variants.len(), 2); + assert_eq!(variants.len(), 3); assert_eq!(variants.pop_first(), Some(UserSignupMethod::EmailPassword)); assert_eq!(variants.pop_first(), Some(UserSignupMethod::GoogleSignIn)); + assert_eq!(variants.pop_first(), Some(UserSignupMethod::StripeCheckout)); assert_eq!(variants.pop_first(), None); } }