diff --git a/app/components/DS/merchant_select.html.erb b/app/components/DS/merchant_select.html.erb
new file mode 100644
index 000000000..e85c15478
--- /dev/null
+++ b/app/components/DS/merchant_select.html.erb
@@ -0,0 +1,101 @@
+
"
+ data-action="click@window->merchant-select#handleOutsideClick keydown->merchant-select#handleKeydown">
+
+
+
>
+
+ <% unless disabled %>
+
+
+ <%= render DS::SearchInput.new(
+ variant: :embedded,
+ class: "flex-1 min-w-0",
+ placeholder: helpers.t("transactions.form.merchant_search_placeholder"),
+ data: {
+ merchant_select_target: "search",
+ action: "input->merchant-select#filter keydown->merchant-select#handleSearchKeydown"
+ }
+ ) %>
+
+
+
+
+
+
+
+ <% end %>
+
diff --git a/app/components/DS/merchant_select.rb b/app/components/DS/merchant_select.rb
new file mode 100644
index 000000000..99649ef7b
--- /dev/null
+++ b/app/components/DS/merchant_select.rb
@@ -0,0 +1,45 @@
+class DS::MerchantSelect < DesignSystemComponent
+ attr_reader :form, :method, :merchants, :selected_id, :disabled, :auto_submit,
+ :menu_placement, :label, :include_blank
+
+ MENU_PLACEMENTS = %w[auto down up].freeze
+
+ def initialize(form:, method:, merchants:, selected_id:, disabled: false, auto_submit: false,
+ menu_placement: :auto, label: nil, include_blank: nil)
+ @form = form
+ @method = method
+ @merchants = merchants
+ @selected_id = selected_id&.to_s
+ @disabled = disabled
+ @auto_submit = auto_submit
+ @menu_placement = normalize_menu_placement(menu_placement)
+ @label = label
+ @include_blank = include_blank
+ end
+
+ def field_name
+ "#{form.object_name}[#{method}]"
+ end
+
+ def menu_id
+ @menu_id ||= "merchant_select_#{field_name.gsub(/\W+/, "_")}_#{object_id}"
+ end
+
+ def selected_merchant
+ merchants.find { |merchant| merchant.id.to_s == selected_id }
+ end
+
+ def selected_merchant_logo_url
+ merchant = selected_merchant
+ return nil unless merchant&.respond_to?(:logo_url) && merchant.logo_url.present?
+
+ Setting.transform_brand_fetch_url(merchant.logo_url)
+ end
+
+ private
+
+ def normalize_menu_placement(value)
+ normalized = value.to_s.downcase
+ MENU_PLACEMENTS.include?(normalized) ? normalized : "auto"
+ end
+end
diff --git a/app/controllers/family_merchants_controller.rb b/app/controllers/family_merchants_controller.rb
index eb3f3a52d..a8bb4580d 100644
--- a/app/controllers/family_merchants_controller.rb
+++ b/app/controllers/family_merchants_controller.rb
@@ -47,9 +47,20 @@ class FamilyMerchantsController < ApplicationController
respond_to do |format|
format.html { redirect_to family_merchants_path, notice: t(".success") }
format.turbo_stream { render turbo_stream: turbo_stream.action(:redirect, family_merchants_path) }
+ format.json { render json: merchant_json(@family_merchant), status: :created }
end
else
- render :new, status: :unprocessable_entity
+ respond_to do |format|
+ # No explicit format.turbo_stream branch: Turbo's form submissions send an
+ # Accept header that prefers turbo-stream, but forcing that format here would
+ # lock the response's Content-Type to turbo-stream while still rendering the
+ # plain :new HTML template — Turbo's client then sees a turbo-stream
+ # Content-Type with no