diff --git a/app/controllers/artifacts_controller.rb b/app/controllers/artifacts_controller.rb new file mode 100644 index 000000000..0093df1f0 --- /dev/null +++ b/app/controllers/artifacts_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Public, shareable "I earned this" page for a single shop purchase. The URL is +# keyed by ShopOrder#signed_id (purpose: :artifact), not the sequential id, so +# purchases can't be enumerated. The page is open to anyone with the link; the +# link is only mintable from the buyer's own orders page (behind :artifact_share). +class ArtifactsController < ApplicationController + def show + @order = ShopOrder.find_signed(params[:id], purpose: :artifact) + if @order.nil? + skip_authorization + return head :not_found + end + + authorize @order, :show?, policy_class: ArtifactPolicy + + @item = @order.shop_item + @buyer = @order.user + @stardust_spent = @order.total_cost_with_modifiers + @featured_project = featured_project_for(@buyer) + end + + private + + # Stardust is a pooled balance, so a purchase isn't tied to a specific ship. + # Feature the buyer's most recent ship-event payout's project as the thing + # they "earned it by shipping". Best-effort: nil falls back to generic copy. + def featured_project_for(user) + LedgerEntry + .where(user: user, ledgerable_type: "Post::ShipEvent") + .order(created_at: :desc) + .first&.ledgerable&.post&.project + rescue StandardError + nil + end +end diff --git a/app/policies/artifact_policy.rb b/app/policies/artifact_policy.rb new file mode 100644 index 000000000..60e3de292 --- /dev/null +++ b/app/policies/artifact_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# A purchase artifact is intentionally public: anyone with the signed link can +# view it. Enumeration is prevented by the signed id (see ArtifactsController), +# not by this policy. +class ArtifactPolicy < ApplicationPolicy + def show? = true +end diff --git a/app/views/artifacts/show.html.erb b/app/views/artifacts/show.html.erb new file mode 100644 index 000000000..0a48d8760 --- /dev/null +++ b/app/views/artifacts/show.html.erb @@ -0,0 +1,44 @@ +<% project = @featured_project %> +<% content_for :title, "#{@buyer.display_name} earned #{@item.name} on Stardance" %> +<% content_for :og_title, "I earned #{@item.name} on Stardance" %> +<% content_for :og_description, project ? "Shipped #{project.title}, earned stardust, and redeemed it for #{@item.name} on Stardance." : "Earned stardust building on Stardance and redeemed it for #{@item.name}." %> +<% content_for :og_image, project_og_image_url(project, format: :png) if project %> +<% content_for :twitter_card, "summary_large_image" %> +<% content_for :head do %> + +<% end %> + +
+

Stardance

+

<%= @buyer.display_name %> earned this

+ +
+ <% if project %> +

+ Earned by shipping <%= project.title %> on Stardance, then redeemed + <%= number_with_precision(@stardust_spent, precision: 0) %> stardust + for <%= @item.name %>. +

+ <% else %> +

+ Earned <%= number_with_precision(@stardust_spent, precision: 0) %> stardust + building on Stardance and redeemed it for <%= @item.name %>. +

+ <% end %> +
+ + <%= link_to "Start building on Stardance →", root_url, + class: "action-btn action-btn--large action-btn--primary idv-setup-card__cta" %> + + +
diff --git a/app/views/shop/orders/index.html.erb b/app/views/shop/orders/index.html.erb index 712e54995..96ab5af8b 100644 --- a/app/views/shop/orders/index.html.erb +++ b/app/views/shop/orders/index.html.erb @@ -39,6 +39,20 @@ <% if (order.pending? || order.aasm_state == "awaiting_periodical_fulfillment") && !order.shop_item.is_a?(ShopItem::FreeStickers) %> <%= button_to "Cancel Order", cancel_shop_order_path(order), method: :delete, class: "my-orders__cancel-link", form: { onsubmit: "return confirm('Are you sure you want to cancel this order?')" } %> <% end %> + <% if Flipper.enabled?(:artifact_share, current_user) %> + <%= render ActionButtonComponent.new( + text: "Share your win →", + type: :button, + size: :small, + variant: :secondary, + data: { + controller: "copy tooltip", + "copy-text-value": artifact_url(order.signed_id(purpose: :artifact)), + "tooltip-message-value": "Click to copy", + action: "copy#copy" + } + ) %> + <% end %> diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index cb8f63aea..8db75512b 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -40,6 +40,7 @@ week_1_release hardware_flow ship_event_payouts + artifact_share ].each { |flag| Flipper.add(flag) } end rescue StandardError => e diff --git a/config/routes.rb b/config/routes.rb index 3ee73737f..35cfd748a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -434,6 +434,10 @@ # Static OG images get "og/:page", to: "og_images#show", as: :og_image, defaults: { format: :png } + + # Shareable purchase artifact (public, keyed by a signed id — see ArtifactsController) + get "artifacts/:id", to: "artifacts#show", as: :artifact + # Landing root "landing#index" get "landing/signup_count", to: "landing#signup_count", as: :landing_signup_count