feat: 🎸 bug fixes
This commit is contained in:
10
bun.lock
10
bun.lock
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
32
vite/src/views/onboarding3/hooks/useSyncPlaygroundMode.tsx
Normal file
32
vite/src/views/onboarding3/hooks/useSyncPlaygroundMode.tsx
Normal 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]);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />}
|
||||
/>
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user