Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

class Api::Internal::CartsController < Api::Internal::BaseController
class Checkout::CartsController < ApplicationController
def update
if permitted_cart_params[:items].length > Cart::MAX_ALLOWED_CART_PRODUCTS
return render json: { error: "You cannot add more than #{Cart::MAX_ALLOWED_CART_PRODUCTS} products to the cart." }, status: :unprocessable_entity
Expand Down
8 changes: 6 additions & 2 deletions app/controllers/checkout_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
class CheckoutController < ApplicationController
before_action :process_cart_id_param

layout "inertia"

def index
@hide_layouts = true
@checkout_presenter = CheckoutPresenter.new(logged_in_user:, ip: request.remote_ip)
render inertia: "Checkout/Index", props: CheckoutPresenter.new(logged_in_user:, ip: request.remote_ip).checkout_props(
params:,
browser_guid: cookies[:_gumroad_guid]
)
end

private
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/components/Checkout/TemporaryLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import * as React from "react";
import { ProductNativeType } from "$app/parsers/product";

import { Creator } from "$app/components/Checkout/cartState";
import { Result } from "$app/components/Checkout/modals";
import { useState } from "$app/components/Checkout/payment";
import { CreateAccountForm } from "$app/components/Checkout/Receipt";
import { useLoggedInUser } from "$app/components/LoggedInUser";
import { AuthorByline } from "$app/components/Product/AuthorByline";
import { Thumbnail } from "$app/components/Product/Thumbnail";
import { showAlert } from "$app/components/server-components/Alert";
import { Result } from "$app/components/server-components/CheckoutPage";
import { Card as UICard, CardContent } from "$app/components/ui/Card";
import { PageHeader } from "$app/components/ui/PageHeader";
import { ProductCard, ProductCardFigure, ProductCardHeader, ProductCardFooter } from "$app/components/ui/ProductCard";
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/components/Checkout/cartState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export function newCartState(): CartState {
export async function saveCartState(cart: CartState) {
const response = await request({
method: "PUT",
url: Routes.internal_cart_path(),
url: Routes.checkout_cart_path(),
accept: "json",
data: { cart },
});
Expand Down
143 changes: 143 additions & 0 deletions app/javascript/components/Checkout/modals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import * as React from "react";

import { LineItemResult } from "$app/data/purchase";
import { formatOrderOfMagnitude } from "$app/utils/formatOrderOfMagnitude";

import { Button } from "$app/components/Button";
import { CartItem, CartState, CrossSell, Upsell, getDiscountedPrice } from "$app/components/Checkout/cartState";
import { AuthorByline } from "$app/components/Product/AuthorByline";
import { computeOptionPrice, OptionRadioButton, Option } from "$app/components/Product/ConfigurationSelector";
import { PriceTag } from "$app/components/Product/PriceTag";
import { ProductCard, ProductCardFigure, ProductCardHeader, ProductCardFooter } from "$app/components/ui/ProductCard";

export type Result = { item: CartItem; result: LineItemResult };

export const CrossSellModal = ({
crossSell,
decline,
accept,
cart,
}: {
crossSell: CrossSell;
accept: () => void;
decline: () => void;
cart: CartState;
}) => {
const product = crossSell.offered_product.product;
const option = product.options.find(({ id }) => id === crossSell.offered_product.option_id);

const crossSellCartItem: CartItem = {
...crossSell.offered_product,
quantity: crossSell.offered_product.quantity || 1,
url_parameters: {},
referrer: "",
recommender_model_name: null,
accepted_offer: crossSell.discount ? { id: crossSell.id, discount: crossSell.discount } : null,
};
const { price: discountedPrice } = getDiscountedPrice(cart, crossSellCartItem);

return (
<>
<div className="grid gap-4">
<h4 dangerouslySetInnerHTML={{ __html: crossSell.description }} />
<ProductCard className="lg:flex-row">
<ProductCardFigure className="lg:w-56 lg:rounded-l lg:rounded-tr-none lg:border-r lg:border-b-0">
{product.thumbnail_url ? <img src={product.thumbnail_url} /> : null}
</ProductCardFigure>
<section className="flex flex-1 flex-col overflow-hidden lg:gap-8 lg:px-6 lg:py-4">
<ProductCardHeader className="lg:border-b-0 lg:p-0">
<a className="stretched-link" href={product.url} target="_blank" rel="noreferrer">
<h3 className="truncate">{option ? `${product.name} - ${option.name}` : product.name}</h3>
</a>
<AuthorByline
name={product.creator.name}
profileUrl={product.creator.profile_url}
avatarUrl={product.creator.avatar_url}
/>
</ProductCardHeader>
<ProductCardFooter className="lg:divide-x-0">
{crossSell.ratings ? (
<div className="flex flex-[1_0_max-content] items-center gap-1 p-4 lg:p-0">
<span className="rating-average">{crossSell.ratings.average.toFixed(1)}</span>
<span>{`(${formatOrderOfMagnitude(crossSell.ratings.count, 1)})`}</span>
</div>
) : null}
<div className="p-4 lg:p-0">
<PriceTag
currencyCode={product.currency_code}
oldPrice={
discountedPrice < crossSell.offered_product.price ? crossSell.offered_product.price : undefined
}
price={discountedPrice}
recurrence={
product.recurrences
? {
id: product.recurrences.default,
duration_in_months: product.duration_in_months,
}
: undefined
}
isPayWhatYouWant={product.is_tiered_membership ? !!option?.is_pwyw : !!product.pwyw}
isSalesLimited={false}
creatorName={product.creator.name}
tooltipPosition="top"
/>
</div>
</ProductCardFooter>
</section>
</ProductCard>
</div>
<footer style={{ display: "grid", gap: "var(--spacer-4)", gridTemplateColumns: "1fr 1fr" }}>
<Button onClick={decline}>
{crossSell.replace_selected_products ? "Don't upgrade" : "Continue without adding"}
</Button>
<Button color="primary" onClick={accept}>
{crossSell.replace_selected_products ? "Upgrade" : "Add to cart"}
</Button>
</footer>
</>
);
};

export type OfferedUpsell = Upsell & { item: CartItem; offeredOption: Option };
export const UpsellModal = ({
upsell,
accept,
decline,
cart,
}: {
upsell: OfferedUpsell;
accept: () => void;
decline: () => void;
cart: CartState;
}) => {
const { item, offeredOption } = upsell;
const product = item.product;
const { discount } = getDiscountedPrice(cart, { ...item, option_id: offeredOption.id });
return (
<>
<div className="flex flex-col gap-4">
<h4 dangerouslySetInnerHTML={{ __html: upsell.description }} />
<div className="radio-buttons" role="radiogroup">
<OptionRadioButton
selected
priceCents={product.price_cents + computeOptionPrice(offeredOption, item.recurrence)}
name={offeredOption.name}
description={offeredOption.description}
currencyCode={product.currency_code}
isPWYW={product.is_tiered_membership ? offeredOption.is_pwyw : !!item.product.pwyw}
discount={discount && discount.type !== "ppp" ? discount.value : null}
recurrence={item.recurrence}
product={product}
/>
</div>
</div>
<footer style={{ display: "grid", gap: "var(--spacer-4)", gridTemplateColumns: "1fr 1fr" }}>
<Button onClick={decline}>Don't upgrade</Button>
<Button color="primary" onClick={accept}>
Upgrade
</Button>
</footer>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AbortError, assertResponseError } from "$app/utils/request";

import { Button } from "$app/components/Button";
import { ProductToAdd, CartItem } from "$app/components/Checkout/cartState";
import { CrossSellModal, UpsellModal } from "$app/components/Checkout/modals";
import { CheckoutPreview } from "$app/components/CheckoutDashboard/CheckoutPreview";
import { DiscountInput, InputtedDiscount } from "$app/components/CheckoutDashboard/DiscountInput";
import { Layout, Page } from "$app/components/CheckoutDashboard/Layout";
Expand All @@ -36,7 +37,6 @@ import { applySelection } from "$app/components/Product/ConfigurationSelector";
import { Search } from "$app/components/Search";
import { Select } from "$app/components/Select";
import { showAlert } from "$app/components/server-components/Alert";
import { CrossSellModal, UpsellModal } from "$app/components/server-components/CheckoutPage";
import { Skeleton } from "$app/components/Skeleton";
import { Card, CardContent } from "$app/components/ui/Card";
import { PageHeader } from "$app/components/ui/PageHeader";
Expand Down
9 changes: 0 additions & 9 deletions app/javascript/packs/checkout.ts

This file was deleted.

Loading