diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb index c67b616ecc..434d50ba41 100644 --- a/app/controllers/subscriptions_controller.rb +++ b/app/controllers/subscriptions_controller.rb @@ -8,10 +8,12 @@ class SubscriptionsController < ApplicationController after_action :verify_authorized, except: PUBLIC_ACTIONS before_action :fetch_subscription, only: %i[unsubscribe_by_seller unsubscribe_by_user magic_link send_magic_link] - before_action :hide_layouts, only: [:manage, :magic_link, :send_magic_link] + before_action :hide_layouts, only: [:magic_link, :send_magic_link] before_action :set_noindex_header, only: [:manage] before_action :check_can_manage, only: [:manage, :unsubscribe_by_user] + layout "inertia", only: [:manage] + SUBSCRIPTION_COOKIE_EXPIRY = 1.week def unsubscribe_by_seller @@ -23,9 +25,10 @@ def unsubscribe_by_seller def unsubscribe_by_user @subscription.cancel!(by_seller: false) - render json: { success: true } + subscription_entity = @subscription.is_installment_plan ? "installment plan" : "membership" + redirect_to manage_subscription_path(@subscription.external_id), notice: "Your #{subscription_entity} has been cancelled." rescue ActiveRecord::RecordInvalid => e - render json: { success: false, error: e.message } + redirect_to manage_subscription_path(@subscription.external_id), alert: e.message end def manage @@ -39,6 +42,10 @@ def manage set_product_page_meta(@product) set_subscription_confirmed_redirect_cookie + + render inertia: "Subscriptions/Manage", + props: CheckoutPresenter.new(logged_in_user: logged_in_user, ip: request.remote_ip) + .subscription_manager_props(subscription: @subscription) end def magic_link diff --git a/app/javascript/data/subscription.ts b/app/javascript/data/subscription.ts index 654d2ffa0d..dce48a282b 100644 --- a/app/javascript/data/subscription.ts +++ b/app/javascript/data/subscription.ts @@ -5,26 +5,7 @@ import { StripeErrorParams, serializeCardParamsIntoQueryParamsObject, } from "$app/data/payment_method_params"; -import { request, ResponseError } from "$app/utils/request"; - -export const cancelSubscriptionByUser = async (subscriptionId: string): Promise => { - const response = await request({ - url: Routes.unsubscribe_by_user_subscription_path(subscriptionId), - method: "POST", - accept: "json", - }); - if (response.ok) { - const responseData = cast<{ success: boolean; redirect_to?: string }>(await response.json()); - if (responseData.success) { - return; - } else if (responseData.redirect_to) { - window.location.href = responseData.redirect_to; - } else { - throw new ResponseError(); - } - } - throw new ResponseError(); -}; +import { request } from "$app/utils/request"; export type UpdateSubscriptionPayload = { cardParams: AnyPaymentMethodParams | StripeErrorParams | null; diff --git a/app/javascript/packs/subscription_edit.ts b/app/javascript/packs/subscription_edit.ts deleted file mode 100644 index f84e789ff0..0000000000 --- a/app/javascript/packs/subscription_edit.ts +++ /dev/null @@ -1,8 +0,0 @@ -import ReactOnRails from "react-on-rails"; - -import BasePage from "$app/utils/base_page"; - -import SubscriptionManager from "$app/components/server-components/SubscriptionManager"; - -BasePage.initialize(); -ReactOnRails.register({ SubscriptionManager }); diff --git a/app/javascript/components/server-components/SubscriptionManager.tsx b/app/javascript/pages/Subscriptions/Manage.tsx similarity index 88% rename from app/javascript/components/server-components/SubscriptionManager.tsx rename to app/javascript/pages/Subscriptions/Manage.tsx index 14001d14b1..95ebfeeca4 100644 --- a/app/javascript/components/server-components/SubscriptionManager.tsx +++ b/app/javascript/pages/Subscriptions/Manage.tsx @@ -1,9 +1,10 @@ +import { router, useForm, usePage } from "@inertiajs/react"; import { parseISO } from "date-fns"; import * as React from "react"; -import { createCast } from "ts-safe-cast"; +import { cast } from "ts-safe-cast"; import { confirmLineItem } from "$app/data/purchase"; -import { cancelSubscriptionByUser, updateSubscription } from "$app/data/subscription"; +import { updateSubscription } from "$app/data/subscription"; import { SavedCreditCard } from "$app/parsers/card"; import { Discount } from "$app/parsers/checkout"; import { CustomFieldDescriptor, ProductNativeType } from "$app/parsers/product"; @@ -13,10 +14,7 @@ import { formatUSDCentsWithExpandedCurrencySymbol, getMinPriceCents, } from "$app/utils/currency"; -import { asyncVoid } from "$app/utils/promise"; import { recurrenceLabels, RecurrenceId } from "$app/utils/recurringPricing"; -import { assertResponseError } from "$app/utils/request"; -import { register } from "$app/utils/serverComponentUtil"; import { Button } from "$app/components/Button"; import { Creator } from "$app/components/Checkout/cartState"; @@ -38,10 +36,9 @@ import { import { showAlert } from "$app/components/server-components/Alert"; import { Alert } from "$app/components/ui/Alert"; import { Card, CardContent } from "$app/components/ui/Card"; +import { useOnChangeSync } from "$app/components/useOnChange"; import { useOriginalLocation } from "$app/components/useOriginalLocation"; -import { useOnChangeSync } from "../useOnChange"; - type Props = { product: { permalink: string; @@ -98,22 +95,23 @@ type Props = { paypal_client_id: string; }; -const SubscriptionManager = ({ - product, - subscription, - recaptcha_key, - paypal_client_id, - contact_info, - countries, - us_states, - ca_provinces, - used_card, -}: Props) => { +export default function SubscriptionsManage() { + const { + product, + subscription, + recaptcha_key, + paypal_client_id, + contact_info, + countries, + us_states, + ca_provinces, + used_card, + } = cast(usePage().props); + const url = new URL(useOriginalLocation()); const subscriptionEntity = subscription.is_installment_plan ? "installment plan" : "membership"; const restartable = !subscription.alive || subscription.pending_cancellation; - const [cancelled, setCancelled] = React.useState(restartable); const initialSelection = { recurrence: subscription.recurrence, rent: false, @@ -209,7 +207,7 @@ const SubscriptionManager = ({ nativeType: product.native_type, canGift: false, }; - const payLabel = cancelled ? `Restart ${subscriptionEntity}` : `Update ${subscriptionEntity}`; + const payLabel = restartable ? `Restart ${subscriptionEntity}` : `Update ${subscriptionEntity}`; const { require_email_typo_acknowledgment } = useFeatureFlags(); const reducer = createReducer({ country: contact_info.country, @@ -268,10 +266,10 @@ const SubscriptionManager = ({ }); if (result.type === "done") { showAlert(result.message, "success"); - setCancelled(false); - setCancellationStatus("initial"); if (result.next != null) { - window.location.href = result.next; + router.visit(result.next); + } else { + router.reload(); } } else if (result.type === "requires_card_action") { await confirmLineItem({ @@ -282,8 +280,7 @@ const SubscriptionManager = ({ }).then((itemResult) => { if (itemResult.success) { showAlert(`Your ${subscriptionEntity} has been updated.`, "success"); - setCancelled(false); - setCancellationStatus("initial"); + router.reload(); } }); } else { @@ -300,20 +297,13 @@ const SubscriptionManager = ({ if (state.status.type === "offering") dispatchAction({ type: "validate" }); }, [state.status.type]); - const [cancellationStatus, setCancellationStatus] = React.useState<"initial" | "processing" | "done">("initial"); - const handleCancel = asyncVoid(async () => { - if (cancellationStatus === "processing" || cancellationStatus === "done") return; - setCancellationStatus("processing"); - try { - await cancelSubscriptionByUser(subscription.id); - setCancellationStatus("done"); - setCancelled(true); - } catch (e) { - assertResponseError(e); - setCancellationStatus("initial"); - showAlert("Sorry, something went wrong.", "error"); - } - }); + const cancelForm = useForm({}); + const unsubscribe = () => { + cancelForm.post(Routes.unsubscribe_by_user_subscription_path(subscription.id), { + preserveScroll: true, + onError: () => showAlert("Sorry, something went wrong.", "error"), + }); + }; const hasSavedCard = state.savedCreditCard != null; const isPendingFirstGifteePayment = subscription.is_gift && subscription.successful_purchases_count === 1; @@ -378,16 +368,14 @@ const SubscriptionManager = ({ ) : null} ); -}; - -export default register({ component: SubscriptionManager, propParser: createCast() }); +} diff --git a/app/javascript/ssr.ts b/app/javascript/ssr.ts index 4a9538e1d5..cd50ea9e9f 100644 --- a/app/javascript/ssr.ts +++ b/app/javascript/ssr.ts @@ -27,7 +27,6 @@ import ProfileProductPage from "$app/components/server-components/Profile/Produc import DisputeEvidencePage from "$app/components/server-components/Purchase/DisputeEvidencePage"; import SecureRedirectPage from "$app/components/server-components/SecureRedirectPage"; import SubscribePage from "$app/components/server-components/SubscribePage"; -import SubscriptionManager from "$app/components/server-components/SubscriptionManager"; import SubscriptionManagerMagicLink from "$app/components/server-components/SubscriptionManagerMagicLink"; import SupportHeader from "$app/components/server-components/support/Header"; import TaxesCollectionModal from "$app/components/server-components/TaxesCollectionModal"; @@ -63,7 +62,6 @@ ReactOnRails.register({ ProfileProductPage, SecureRedirectPage, SubscribePage, - SubscriptionManager, SubscriptionManagerMagicLink, TaxesCollectionModal, VideoStreamPlayer, diff --git a/app/views/subscriptions/manage.html.erb b/app/views/subscriptions/manage.html.erb deleted file mode 100644 index 8630051310..0000000000 --- a/app/views/subscriptions/manage.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -<%= render("shared/recaptcha_script") %> -<%= load_pack("subscription_edit") %> - -<%= react_component "SubscriptionManager", props: CheckoutPresenter.new(logged_in_user: logged_in_user, ip: request.remote_ip).subscription_manager_props(subscription: @subscription), prerender: true %> diff --git a/spec/controllers/subscriptions_controller_spec.rb b/spec/controllers/subscriptions_controller_spec.rb index 1826ace5c3..2c7405ee88 100644 --- a/spec/controllers/subscriptions_controller_spec.rb +++ b/spec/controllers/subscriptions_controller_spec.rb @@ -66,9 +66,10 @@ post :unsubscribe_by_user, params: { id: @subscription.external_id } end - it "returns json success" do + it "redirects to manage page with success notice" do post :unsubscribe_by_user, params: { id: @subscription.external_id } - expect(response.parsed_body["success"]).to be(true) + expect(response).to redirect_to(manage_subscription_path(@subscription.external_id)) + expect(flash[:notice]).to eq("Your membership has been cancelled.") end it "is not allowed for installment plans" do @@ -79,8 +80,8 @@ post :unsubscribe_by_user, params: { id: subscription.external_id } - expect(response.parsed_body["success"]).to be(false) - expect(response.parsed_body["error"]).to include("Installment plans cannot be cancelled by the customer") + expect(response).to redirect_to(manage_subscription_path(subscription.external_id)) + expect(flash[:alert]).to include("Installment plans cannot be cancelled by the customer") end context "when the encrypted cookie is not present" do @@ -88,13 +89,12 @@ cookies.encrypted[@subscription.cookie_key] = nil end - it "renders success false with redirect_to URL" do + it "redirects to magic link page" do expect do - post :unsubscribe_by_user, params: { id: @subscription.external_id }, format: :json + post :unsubscribe_by_user, params: { id: @subscription.external_id } end.to_not change { @subscription.reload.user_requested_cancellation_at } - expect(response.parsed_body["success"]).to be(false) - expect(response.parsed_body["redirect_to"]).to eq(magic_link_subscription_path(@subscription.external_id)) + expect(response).to redirect_to(magic_link_subscription_path(@subscription.external_id)) end end end diff --git a/spec/requests/subscription/non_tiered_membership_spec.rb b/spec/requests/subscription/non_tiered_membership_spec.rb index 3ddf7ad28d..8a37375d1a 100644 --- a/spec/requests/subscription/non_tiered_membership_spec.rb +++ b/spec/requests/subscription/non_tiered_membership_spec.rb @@ -204,10 +204,10 @@ visit "/subscriptions/#{@subscription_with_purchaser.external_id}/manage?token=#{@subscription_with_purchaser.token}" click_on "Cancel membership" - wait_for_ajax - expect(page).to have_button("Cancelled", disabled: true) + expect(page).to have_alert(text: "Your membership has been cancelled.") expect(page).to have_button("Restart membership") + expect(page).not_to have_button("Cancel membership") click_on "Restart membership" wait_for_ajax