mirror of
https://github.com/we-promise/sure.git
synced 2026-09-05 23:01:26 +00:00
Fix onboarding country and currency defaults (#3070)
* fix: derive onboarding currency from country * feat: use browser locale for onboarding defaults * Fix onboarding currency defaults * Preserve saved onboarding currency during hydration --------- Co-authored-by: sure-admin <sure-admin@splashblot.com>
This commit is contained in:
@@ -2,16 +2,20 @@ import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Connects to data-controller="onboarding"
|
||||
export default class extends Controller {
|
||||
static targets = ["nameField", "monikerRadio"]
|
||||
static targets = ["nameField", "monikerRadio", "countryField", "currencyField"]
|
||||
static values = {
|
||||
householdNameLabel: String,
|
||||
householdNamePlaceholder: String,
|
||||
groupNameLabel: String,
|
||||
groupNamePlaceholder: String
|
||||
groupNamePlaceholder: String,
|
||||
country: String,
|
||||
countryCurrencies: Object,
|
||||
currencyOverride: Boolean
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.updateNameFieldForCurrentMoniker();
|
||||
this.applyBrowserLocaleDefaults();
|
||||
}
|
||||
|
||||
setLocale(event) {
|
||||
@@ -30,6 +34,43 @@ export default class extends Controller {
|
||||
document.documentElement.setAttribute("data-theme", event.target.value);
|
||||
}
|
||||
|
||||
applyBrowserLocaleDefaults() {
|
||||
const browserCountry = this.browserCountry();
|
||||
|
||||
if (this.hasCountryFieldTarget && browserCountry && this.countryFieldTarget.value === "US" && this.hasCountryOption(browserCountry)) {
|
||||
this.countryFieldTarget.value = browserCountry;
|
||||
}
|
||||
|
||||
if (this.hasCurrencyFieldTarget && !this.currencyOverrideValue) {
|
||||
const country = this.countryValue || (this.hasCountryFieldTarget ? this.countryFieldTarget.value : null) || browserCountry;
|
||||
const currency = this.currencyForCountry(country);
|
||||
|
||||
if (currency && this.hasCurrencyOption(currency)) {
|
||||
this.currencyFieldTarget.value = currency;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
browserCountry() {
|
||||
try {
|
||||
return new Intl.Locale(navigator.language).maximize().region;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
currencyForCountry(country) {
|
||||
return (this.hasCountryCurrenciesValue ? this.countryCurrenciesValue : {})[country?.toUpperCase()];
|
||||
}
|
||||
|
||||
hasCountryOption(country) {
|
||||
return Array.from(this.countryFieldTarget.options).some((option) => option.value === country);
|
||||
}
|
||||
|
||||
hasCurrencyOption(currency) {
|
||||
return Array.from(this.currencyFieldTarget.options).some((option) => option.value === currency);
|
||||
}
|
||||
|
||||
updateNameFieldForCurrentMoniker(event = null) {
|
||||
if (!this.hasNameFieldTarget) {
|
||||
return;
|
||||
|
||||
+23
-10
@@ -151,7 +151,28 @@ class Family < ApplicationRecord
|
||||
before_validation :normalize_enabled_currencies!
|
||||
|
||||
def primary_currency_code
|
||||
normalize_currency_code(currency) || "USD"
|
||||
self.class.normalize_currency_code(currency) || "USD"
|
||||
end
|
||||
|
||||
def default_currency_for_country
|
||||
self.class.default_currency_for_country(country)
|
||||
end
|
||||
|
||||
def self.default_currency_for_country(country)
|
||||
country_currency = ISO3166::Country.new(country.to_s.upcase)&.currency_code
|
||||
normalize_currency_code(country_currency) || "USD"
|
||||
end
|
||||
|
||||
def self.default_currency_by_country
|
||||
LanguagesHelper::COUNTRY_MAPPING.keys.index_with { |country| default_currency_for_country(country) }
|
||||
end
|
||||
|
||||
def self.normalize_currency_code(value)
|
||||
return if value.blank?
|
||||
|
||||
Money::Currency.new(value).iso_code
|
||||
rescue Money::Currency::UnknownCurrencyError, ArgumentError
|
||||
nil
|
||||
end
|
||||
|
||||
def custom_enabled_currencies?
|
||||
@@ -491,15 +512,7 @@ class Family < ApplicationRecord
|
||||
end
|
||||
|
||||
def normalize_currency_codes(values)
|
||||
Array(values).filter_map { |value| normalize_currency_code(value) }.uniq
|
||||
end
|
||||
|
||||
def normalize_currency_code(value)
|
||||
return if value.blank?
|
||||
|
||||
Money::Currency.new(value).iso_code
|
||||
rescue Money::Currency::UnknownCurrencyError, ArgumentError
|
||||
nil
|
||||
Array(values).filter_map { |value| self.class.normalize_currency_code(value) }.uniq
|
||||
end
|
||||
|
||||
# Not a plain `inclusion: { in: ActiveSupport::TimeZone.all.map(&:name) }`
|
||||
|
||||
@@ -8,7 +8,15 @@
|
||||
<%= render "onboardings/logout" %>
|
||||
<% end %>
|
||||
|
||||
<div class="grow max-w-lg w-full mx-auto bg-surface flex flex-col justify-center md:py-0 py-6 px-4 md:px-0" data-controller="onboarding">
|
||||
<% currency_override = Family.normalize_currency_code(params[:currency]) %>
|
||||
<% saved_currency = @user.family.primary_currency_code if @user.set_onboarding_preferences_at? %>
|
||||
<% selected_currency = currency_override || saved_currency || @user.family.default_currency_for_country %>
|
||||
|
||||
<div class="grow max-w-lg w-full mx-auto bg-surface flex flex-col justify-center md:py-0 py-6 px-4 md:px-0"
|
||||
data-controller="onboarding"
|
||||
data-onboarding-country-value="<%= @user.family.country %>"
|
||||
data-onboarding-country-currencies-value="<%= Family.default_currency_by_country.to_json %>"
|
||||
data-onboarding-currency-override-value="<%= currency_override.present? || saved_currency.present? %>">
|
||||
<div>
|
||||
<div class="space-y-1 mb-6">
|
||||
<h1 class="text-2xl font-medium"><%= t(".title") %></h1>
|
||||
@@ -19,10 +27,10 @@
|
||||
<div class="bg-container p-4 rounded-lg flex gap-8 shadow-border-xs">
|
||||
<div class="space-y-2">
|
||||
<%= tag.p t(".example"), class: "text-secondary text-sm" %>
|
||||
<%= tag.p format_money(Money.new(2325.25, params[:currency] || @user.family.currency)), class: "text-primary font-medium text-2xl" %>
|
||||
<%= tag.p format_money(Money.new(2325.25, selected_currency)), class: "text-primary font-medium text-2xl" %>
|
||||
<p class="text-sm">
|
||||
<span class="text-green-500 font-medium">+<%= format_money(Money.new(78.90, params[:currency] || @user.family.currency)) %></span>
|
||||
<span class="text-green-500 font-medium">(+<%= format_money(Money.new(6.39, params[:currency] || @user.family.currency)) %>)</span>
|
||||
<span class="text-green-500 font-medium">+<%= format_money(Money.new(78.90, selected_currency)) %></span>
|
||||
<span class="text-green-500 font-medium">(+<%= format_money(Money.new(6.39, selected_currency)) %>)</span>
|
||||
<span class="text-secondary">as of <%= format_date(Date.parse("2024-10-23"), :default, format_code: params[:date_format] || @user.family.date_format) %></span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -81,8 +89,8 @@
|
||||
|
||||
<%= family_form.select :currency,
|
||||
Money::Currency.as_options.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
|
||||
{ label: t(".currency"), required: true, selected: params[:currency] || @user.family.currency },
|
||||
{ data: { action: "onboarding#setCurrency" } } %>
|
||||
{ label: t(".currency"), required: true, selected: selected_currency },
|
||||
{ data: { action: "onboarding#setCurrency", onboarding_target: "currencyField" } } %>
|
||||
|
||||
<%= family_form.select :date_format,
|
||||
Family::DATE_FORMATS,
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
<%= render "onboardings/logout" %>
|
||||
<% end %>
|
||||
|
||||
<div class="grow max-w-lg w-full mx-auto bg-surface flex flex-col justify-center md:py-0 py-2 px-4 md:px-0">
|
||||
<div class="grow max-w-lg w-full mx-auto bg-surface flex flex-col justify-center md:py-0 py-2 px-4 md:px-0"
|
||||
data-controller="onboarding"
|
||||
data-onboarding-household-name-label-value="<%= t(".household_name") %>"
|
||||
data-onboarding-household-name-placeholder-value="<%= t(".household_name_placeholder") %>"
|
||||
data-onboarding-group-name-label-value="<%= t(".group_name") %>"
|
||||
data-onboarding-group-name-placeholder-value="<%= t(".group_name_placeholder") %>">
|
||||
<div>
|
||||
<div class="space-y-1 mb-6 text-center">
|
||||
<h1 class="text-2xl font-medium md:text-2xl"><%= t(".title") %></h1>
|
||||
@@ -34,12 +39,7 @@
|
||||
<div class="space-y-4 mb-4">
|
||||
<%= form.fields_for :family do |family_form| %>
|
||||
<div class="bg-container rounded-lg shadow-border-xs p-4">
|
||||
<div class="space-y-2"
|
||||
data-controller="onboarding"
|
||||
data-onboarding-household-name-label-value="<%= t(".household_name") %>"
|
||||
data-onboarding-household-name-placeholder-value="<%= t(".household_name_placeholder") %>"
|
||||
data-onboarding-group-name-label-value="<%= t(".group_name") %>"
|
||||
data-onboarding-group-name-placeholder-value="<%= t(".group_name_placeholder") %>">
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-primary"><%= t(".moniker_prompt", product_name: product_name) %></p>
|
||||
|
||||
<label class="flex items-center gap-2 text-sm text-primary">
|
||||
@@ -67,7 +67,7 @@
|
||||
<%= family_form.select :country,
|
||||
country_options,
|
||||
{ label: t(".country") },
|
||||
required: true %>
|
||||
{ required: true, data: { onboarding_target: "countryField" } } %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -109,6 +109,44 @@ class OnboardingsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_select "span", text: /\+\$78\.90/
|
||||
end
|
||||
|
||||
test "preferences page derives currency from the onboarding country" do
|
||||
@family.update!(country: "CA", currency: "USD")
|
||||
|
||||
get preferences_onboarding_url
|
||||
|
||||
assert_response :success
|
||||
assert_select "select[name='user[family_attributes][currency]'] option[selected][value='CAD']"
|
||||
end
|
||||
|
||||
test "preferences page keeps the saved currency after preferences have been set" do
|
||||
@family.update!(country: "CA", currency: "USD")
|
||||
@user.update!(set_onboarding_preferences_at: Time.current)
|
||||
|
||||
get preferences_onboarding_url
|
||||
|
||||
assert_response :success
|
||||
assert_select "select[name='user[family_attributes][currency]'] option[selected][value='USD']"
|
||||
end
|
||||
|
||||
test "preferences page preserves an explicit currency override" do
|
||||
@family.update!(country: "CA", currency: "USD")
|
||||
|
||||
get preferences_onboarding_url, params: { currency: "USD" }
|
||||
|
||||
assert_response :success
|
||||
assert_select "select[name='user[family_attributes][currency]'] option[selected][value='USD']"
|
||||
end
|
||||
|
||||
test "preferences page ignores an unsupported currency override" do
|
||||
@family.update!(country: "CA", currency: "USD")
|
||||
|
||||
get preferences_onboarding_url, params: { currency: "NOPE" }
|
||||
|
||||
assert_response :success
|
||||
assert_select "[data-onboarding-currency-override-value='false']"
|
||||
assert_select "select[name='user[family_attributes][currency]'] option[selected][value='CAD']"
|
||||
end
|
||||
|
||||
test "preferences page shows date formatting example" do
|
||||
get preferences_onboarding_url
|
||||
assert_response :success
|
||||
|
||||
@@ -165,6 +165,13 @@ class FamilyTest < ActiveSupport::TestCase
|
||||
assert_equal "Groups", family.moniker_label_plural
|
||||
end
|
||||
|
||||
test "default currency comes from country ISO data" do
|
||||
assert_equal "CAD", Family.default_currency_for_country("CA")
|
||||
assert_equal "EUR", Family.default_currency_for_country("DE")
|
||||
assert_equal "BRL", Family.default_currency_for_country("BR")
|
||||
assert_equal "USD", Family.default_currency_for_country("unknown")
|
||||
end
|
||||
|
||||
test "available_merchants includes family merchants without transactions" do
|
||||
family = families(:dylan_family)
|
||||
|
||||
|
||||
@@ -48,6 +48,38 @@ class OnboardingsTest < ApplicationSystemTestCase
|
||||
assert_text I18n.t("onboardings.goals.title")
|
||||
end
|
||||
|
||||
test "browser locale defaults onboarding country and currency" do
|
||||
@family.update!(country: "US", currency: "USD")
|
||||
set_browser_language("en-CA")
|
||||
|
||||
visit onboarding_path
|
||||
|
||||
assert_equal "CA", find("#user_family_attributes_country").value
|
||||
|
||||
click_button I18n.t("onboardings.show.submit")
|
||||
assert_current_path preferences_onboarding_path
|
||||
|
||||
assert_equal "CAD", find("#user_family_attributes_currency").value
|
||||
|
||||
click_button I18n.t("onboardings.preferences.submit")
|
||||
assert_current_path goals_onboarding_path
|
||||
|
||||
@family.reload
|
||||
assert_equal "CA", @family.country
|
||||
assert_equal "CAD", @family.currency
|
||||
end
|
||||
|
||||
test "saved currency stays authoritative after onboarding controller connects" do
|
||||
@family.update!(country: "CA", currency: "USD")
|
||||
@user.update!(set_onboarding_preferences_at: Time.current)
|
||||
set_browser_language("en-CA")
|
||||
|
||||
visit preferences_onboarding_path
|
||||
|
||||
assert_selector "[data-onboarding-currency-override-value='true']"
|
||||
assert_equal "USD", find("#user_family_attributes_currency").value
|
||||
end
|
||||
|
||||
test "preferences page renders chart without errors" do
|
||||
visit preferences_onboarding_path
|
||||
|
||||
@@ -194,6 +226,18 @@ class OnboardingsTest < ApplicationSystemTestCase
|
||||
.select_option
|
||||
end
|
||||
|
||||
def set_browser_language(language)
|
||||
page.driver.browser.execute_cdp(
|
||||
"Page.addScriptToEvaluateOnNewDocument",
|
||||
source: <<~JS
|
||||
Object.defineProperty(navigator, "language", {
|
||||
get: () => "#{language}",
|
||||
configurable: true
|
||||
});
|
||||
JS
|
||||
)
|
||||
end
|
||||
|
||||
def sign_in(user)
|
||||
visit new_session_path
|
||||
within %(form[action='#{sessions_path}']) do
|
||||
|
||||
Reference in New Issue
Block a user