feat: 🎸 bug fixes

This commit is contained in:
amianthus
2025-10-21 14:06:33 +01:00
parent 622aea4229
commit d4657d63da
16 changed files with 110 additions and 27 deletions

View File

@@ -108,7 +108,7 @@
"@types/express": "^5.0.3",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.10",
"@types/node": "^22.13.4",
"@types/node": "^24.9.1",
"@types/pg": "^8.11.10",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
@@ -2942,7 +2942,9 @@
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"@autumn/server/@types/node": ["@types/node@22.18.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ=="],
"@autumn/server/@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
"@autumn/shared/@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
"@autumn/shared/stripe": ["stripe@18.5.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA=="],
@@ -3498,7 +3500,9 @@
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@autumn/server/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"@autumn/server/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@autumn/shared/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"@autumn/vite/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],

View File

@@ -109,7 +109,7 @@
"@types/express": "^5.0.3",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.10",
"@types/node": "^22.13.4",
"@types/node": "^24.9.1",
"@types/pg": "^8.11.10",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",

View File

@@ -23,7 +23,11 @@ export const handleGetOAuthUrl = createRoute({
// Generate OAuth state and store in Redis
const frontendUrl = process.env.CLIENT_URL || "http://localhost:5173";
const redirectUri = `${frontendUrl}/dev?tab=stripe`;
// Determine redirect URI based on context
const fromOnboarding = c.req.query("from_onboarding") === "true";
const redirectUri = fromOnboarding
? `${frontendUrl}/sandbox/onboarding3?step=playground&m=p`
: `${frontendUrl}/dev?tab=stripe`;
const stateKey = await generateOAuthState({
organizationSlug: org.slug,

View File

@@ -54,14 +54,8 @@ export const handleOAuthCallback = async (c: Context<HonoEnv>) => {
const env = envStr === "live" ? AppEnv.Live : AppEnv.Sandbox;
const isPlatformFlow = master_org_id !== null;
// Use custom redirect URI if provided (platform flow)
if (isPlatformFlow) {
redirectUrl = new URL(redirect_uri);
} else {
redirectUrl = new URL(
`${frontendUrl}${env === AppEnv.Sandbox ? "/sandbox" : ""}/dev?tab=stripe`,
);
}
// Use redirect URI from state (supports both platform and standard flows)
redirectUrl = new URL(redirect_uri);
// Fetch the organization by slug
const org = await OrgService.getBySlug({ db, slug: organization_slug });

View File

@@ -28,13 +28,19 @@ export default function PricingTablePreview({
}
const handleSubscribe = async (product: ProductV2) => {
// Check if Stripe is connected
if (!org?.stripe_connected) {
setConnectStripeOpen(true);
return;
}
if (product.id) {
try {
await checkout({
productId: product.id,
dialog: OnboardingCheckoutDialog,
openInNewTab: true,
successUrl: `${window.location.origin}/sandbox/onboarding3`,
successUrl: `${window.location.origin}/sandbox/onboarding?step=playground&m=p`,
});
} catch (error) {
console.error("Checkout error:", error);

View File

@@ -63,6 +63,7 @@ html {
--t10: #d1d1d1;
--t11: #e3e3e3;
--t12: #444444;
--t13: #FAFAF9;
--radius: 0.375rem; /* 6px */
--primary: #8838ff;
@@ -72,6 +73,7 @@ html {
--radio-group-hover-primary: #fcfaff;
--border: #ddd;
--border-sheet: #EAEAE9
--card-border: #d1d1d1;
--input: #dddddd;
@@ -181,6 +183,7 @@ html {
--color-t10: var(--t10);
--color-t11: var(--t11);
--color-t12: var(--t12);
--color-t13: var(--t13);
--color-input: var(--input);
--color-icon1: var(--icon1);

View File

@@ -37,7 +37,7 @@ export default function ConnectStripeDialog({
const handleRedirectToOAuth = async () => {
try {
const { data } = await axiosInstance.get(
`/v1/organization/stripe/oauth_url`,
`/v1/organization/stripe/oauth_url?from_onboarding=true`,
);
window.open(data.oauth_url, "_blank");
} catch (error) {

View File

@@ -20,6 +20,7 @@ import { useOnboarding3QueryState } from "./hooks/useOnboarding3QueryState";
import { useOnboardingFeatureSync } from "./hooks/useOnboardingFeatureSync";
import { useOnboardingLogic } from "./hooks/useOnboardingLogic";
import { useOnboardingProductSync } from "./hooks/useOnboardingProductSync";
import { useSyncPlaygroundMode } from "./hooks/useSyncPlaygroundMode";
import { OnboardingPreview } from "./OnboardingPreview";
import { OnboardingStep } from "./utils/onboardingUtils";
@@ -53,6 +54,9 @@ export default function OnboardingContent() {
// Initialize onboarding logic and store handlers
useOnboardingLogic();
// Sync playground mode with query params
useSyncPlaygroundMode();
// Track sign-up event on first mount
useEffect(() => {
trackSignUp();
@@ -82,7 +86,7 @@ export default function OnboardingContent() {
className={cn(
"relative w-full h-full flex bg-gray-medium [scrollbar-gutter:stable]",
step === OnboardingStep.Integration
? "overflow-y-auto"
? "overflow-y-auto bg-t13"
: "overflow-y-hidden",
)}
>

View File

@@ -13,6 +13,7 @@ import {
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
import { useProductStore } from "@/hooks/stores/useProductStore";
import CreatePlanDialog from "@/views/products/products/components/CreatePlanDialog";
import { useOnboarding3QueryState } from "../../hooks/useOnboarding3QueryState";
import { useOnboardingStore } from "../../store/useOnboardingStore";
export const PlaygroundToolbar = () => {
@@ -22,7 +23,9 @@ export const PlaygroundToolbar = () => {
// Get current product and playground mode from stores
const product = useProductStore((s) => s.product);
const playgroundMode = useOnboardingStore((s) => s.playgroundMode);
const setPlaygroundMode = useOnboardingStore((s) => s.setPlaygroundMode);
// Get query state setters to update URL
const { setQueryStates } = useOnboarding3QueryState();
// Get handlers from store
const handlePlanSelect = useOnboardingStore((s) => s.handlePlanSelect);
@@ -31,7 +34,10 @@ export const PlaygroundToolbar = () => {
<div className="flex gap-2 items-center justify-between mt-4">
<GroupedTabButton
value={playgroundMode}
onValueChange={(val) => setPlaygroundMode(val as "edit" | "preview")}
onValueChange={(val) => {
// Update query param, which will sync to store
setQueryStates({ m: val === "edit" ? "e" : "p" });
}}
options={[
{
value: "edit",

View File

@@ -12,6 +12,7 @@ export const useOnboarding3QueryState = () => {
OnboardingStep.Integration,
] as const).withDefault(OnboardingStep.PlanDetails),
product_id: parseAsString,
m: parseAsStringLiteral(["e", "p"] as const),
},
{
history: "push",

View File

@@ -0,0 +1,32 @@
import { useEffect } from "react";
import { useOnboardingStore } from "../store/useOnboardingStore";
import { OnboardingStep } from "../utils/onboardingUtils";
import { useOnboarding3QueryState } from "./useOnboarding3QueryState";
/**
* One-way sync: query param → store
* Query param is the source of truth
*/
export const useSyncPlaygroundMode = () => {
const { queryStates, setQueryStates } = useOnboarding3QueryState();
const setPlaygroundMode = useOnboardingStore((s) => s.setPlaygroundMode);
const isPlaygroundStep = queryStates.step === OnboardingStep.Playground;
// ONE-WAY sync: query param → store (only in playground step)
useEffect(() => {
if (isPlaygroundStep) {
// Initialize m param if not present
if (!queryStates.m) {
setQueryStates({ m: "e" });
} else if (queryStates.m === "e") {
setPlaygroundMode("edit");
} else if (queryStates.m === "p") {
setPlaygroundMode("preview");
}
} else if (queryStates.m !== null) {
// Remove 'm' param when not in playground step
setQueryStates({ m: null });
}
}, [queryStates.m, queryStates.step, setPlaygroundMode, setQueryStates, isPlaygroundStep]);
};

View File

@@ -5,8 +5,8 @@ import { useSheetStore } from "@/hooks/stores/useSheetStore";
import { getItemId } from "@/utils/product/productItemUtils";
import { ProductItemContext } from "../product/product-item/ProductItemContext";
import { EditPlanFeatureSheet } from "./components/edit-plan-feature/EditPlanFeatureSheet";
import { EditPlanSheet } from "./components/EditPlanSheet";
import { EditPlanFeatureSheet } from "./components/edit-plan-feature/EditPlanFeatureSheet";
import { NewFeatureSheet } from "./components/new-feature/NewFeatureSheet";
import { SelectFeatureSheet } from "./components/SelectFeatureSheet";
@@ -73,7 +73,7 @@ export const ProductSheets = () => {
};
return (
<SheetContainer className="w-full min-w-xs max-w-md bg-card z-50 border-l shadow-sm h-full">
<SheetContainer className="w-full min-w-xs max-w-md bg-card z-50 border-l shadow-sm h-full border-l-[#EAEAE9]">
{renderSheet()}
</SheetContainer>
);

View File

@@ -29,7 +29,15 @@ export function SelectFeatureSheet({
const setProduct = useProductStore((s) => s.setProduct);
const setSheet = useSheetStore((s) => s.setSheet);
const filteredFeatures = features.filter((f: Feature) => !f.archived);
// Get feature IDs that are already added to the plan
const addedFeatureIds = new Set(
product.items?.map((item) => item.feature_id).filter(Boolean) || []
);
// Filter out archived features and features already on the plan
const filteredFeatures = features.filter(
(f: Feature) => !f.archived && !addedFeatureIds.has(f.id)
);
const handleFeatureSelect = (featureId: string) => {
if (!featureId || !product) return;

View File

@@ -24,6 +24,14 @@ export function BillingType() {
// Derive billing type from item state
const isFeaturePrice = isFeaturePriceItem(item);
// Determine if we should preselect based on explicit configuration
const hasExplicitConfig =
isFeaturePrice || // Has tiers, so it's priced
(item.included_usage !== undefined && item.included_usage !== null) || // Has explicit included usage
item.usage_model !== undefined; // Has explicit usage model
const shouldPreselect = hasExplicitConfig;
const setBillingType = (type: "included" | "priced") => {
const getPricedInterval = () => {
if (
@@ -87,7 +95,7 @@ export function BillingType() {
<div className="mt-3 space-y-4 billing-type-section">
<div className="flex w-full items-center gap-4">
<PanelButton
isSelected={!isFeaturePrice}
isSelected={shouldPreselect && !isFeaturePrice}
onClick={() => setBillingType("included")}
icon={<IncludedUsageIcon size={18} color="currentColor" />}
/>
@@ -105,7 +113,7 @@ export function BillingType() {
<div className="flex w-full items-center gap-4">
<PanelButton
isSelected={isFeaturePrice}
isSelected={shouldPreselect && isFeaturePrice}
onClick={() => setBillingType("priced")}
icon={<CoinsIcon size={20} color="currentColor" />}
/>

View File

@@ -2,6 +2,7 @@ import { PlusIcon } from "@phosphor-icons/react";
import { Button } from "@/components/v2/buttons/Button";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
import { useSheetStore } from "@/hooks/stores/useSheetStore";
import { useProductStore } from "@/hooks/stores/useProductStore";
interface AddFeatureRowProps {
disabled?: boolean;
@@ -11,13 +12,25 @@ interface AddFeatureRowProps {
export const AddFeatureRow = ({ disabled }: AddFeatureRowProps) => {
const { features } = useFeaturesQuery();
const setSheet = useSheetStore((s) => s.setSheet);
const product = useProductStore((s) => s.product);
const handleAddFeatureClick = () => {
if (features.length === 0) {
// No features exist, go directly to create flow
// Get feature IDs that are already added to the plan
const addedFeatureIds = new Set(
product.items?.map((item) => item.feature_id).filter(Boolean) || []
);
// Filter out features that are already on the plan
const availableFeatures = features.filter(
(feature) => !addedFeatureIds.has(feature.id)
);
if (availableFeatures.length === 0) {
// No features available to add (either none exist or all are already added)
// Go directly to create flow
setSheet({ type: "new-feature", itemId: "new" });
} else {
// Features exist, open select sheet
// Features available to add, open select sheet
setSheet({ type: "select-feature", itemId: "select" });
}
};

View File

@@ -167,7 +167,7 @@ export const PlanFeatureRow = ({
size="sm"
variant="skeleton"
tabIndex={-1}
className="absolute right-0 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-50 bg-hover-primary"
className="absolute right-0 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-50 bg-hover-primary group-hover:bg-transparent"
/>
</div>