feat: 🎸 basic frontend for reward feature grants
This commit is contained in:
@@ -37,6 +37,12 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
if (import.meta.env.DEV) {
|
||||
import("react-grab");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-grab": "^0.1.29",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-router": "^7.3.0",
|
||||
"react-router-dom": "^7.6.2",
|
||||
|
||||
@@ -11,6 +11,7 @@ const DEFAULT_REWARD: FrontendReward = {
|
||||
free_product_id: null,
|
||||
discount_config: defaultDiscountConfig,
|
||||
free_product_config: null,
|
||||
featureGrantEntitlements: [],
|
||||
};
|
||||
|
||||
interface RewardState {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SheetContent,
|
||||
SheetTrigger,
|
||||
} from "@/components/v2/sheets/Sheet";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
|
||||
import { useRewardStore } from "@/hooks/stores/useRewardStore";
|
||||
import { RewardService } from "@/services/products/RewardService";
|
||||
@@ -19,6 +20,7 @@ import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { getBackendErr } from "@/utils/genUtils";
|
||||
import { mapFrontendToApiReward } from "../../utils/rewardMappers";
|
||||
import { DiscountRewardConfig } from "./DiscountRewardConfig";
|
||||
import { FeatureGrantRewardConfig } from "./FeatureGrantRewardConfig";
|
||||
import { FreeProductRewardConfig } from "./FreeProductRewardConfig";
|
||||
import { RewardDetails } from "./RewardDetails";
|
||||
import { SelectRewardType } from "./SelectRewardType";
|
||||
@@ -34,6 +36,7 @@ export function CreateRewardSheet({
|
||||
}: CreateRewardSheetProps = {}) {
|
||||
const axiosInstance = useAxiosInstance();
|
||||
const { refetch } = useRewardsQuery();
|
||||
const { features } = useFeaturesQuery();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
@@ -86,9 +89,31 @@ export function CreateRewardSheet({
|
||||
return;
|
||||
}
|
||||
|
||||
if (reward.rewardCategory === "feature_grant") {
|
||||
const validEntitlements = reward.featureGrantEntitlements.filter(
|
||||
(e) => e.feature_id && e.allowance > 0,
|
||||
);
|
||||
if (validEntitlements.length === 0) {
|
||||
toast.error(
|
||||
"Please add at least one entitlement with a feature and balance",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!reward.promo_codes?.length ||
|
||||
!reward.promo_codes.some((pc) => pc.code)
|
||||
) {
|
||||
toast.error("Please add at least one promo code");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const apiReward = mapFrontendToApiReward(reward);
|
||||
const apiReward = mapFrontendToApiReward({
|
||||
frontendReward: reward,
|
||||
features,
|
||||
});
|
||||
|
||||
await RewardService.createReward({
|
||||
axiosInstance,
|
||||
@@ -135,6 +160,10 @@ export function CreateRewardSheet({
|
||||
{reward.rewardCategory === "free_product" && (
|
||||
<FreeProductRewardConfig reward={reward} setReward={setReward} />
|
||||
)}
|
||||
|
||||
{reward.rewardCategory === "feature_grant" && (
|
||||
<FeatureGrantRewardConfig reward={reward} setReward={setReward} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SheetFooter>
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
import {
|
||||
type Feature,
|
||||
FeatureGrantDuration,
|
||||
FeatureType,
|
||||
} from "@autumn/shared";
|
||||
import { PlusIcon, TrashIcon } from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { FormLabel } from "@/components/v2/form/FormLabel";
|
||||
import { Input } from "@/components/v2/inputs/Input";
|
||||
import { SearchableSelect } from "@/components/v2/selects/SearchableSelect";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/v2/selects/Select";
|
||||
import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import type {
|
||||
FrontendReward,
|
||||
FrontendRewardEntitlement,
|
||||
} from "../../types/frontendReward";
|
||||
|
||||
interface FeatureGrantRewardConfigProps {
|
||||
reward: FrontendReward;
|
||||
setReward: (reward: FrontendReward) => void;
|
||||
}
|
||||
|
||||
export function FeatureGrantRewardConfig({
|
||||
reward,
|
||||
setReward,
|
||||
}: FeatureGrantRewardConfigProps) {
|
||||
const { features } = useFeaturesQuery();
|
||||
|
||||
// Filter to metered, non-boolean features only
|
||||
const meteredFeatures = features.filter(
|
||||
(f) => f.type === FeatureType.Metered,
|
||||
);
|
||||
|
||||
const entitlements = reward.featureGrantEntitlements;
|
||||
|
||||
const updateEntitlement = ({
|
||||
index,
|
||||
updates,
|
||||
}: {
|
||||
index: number;
|
||||
updates: Partial<FrontendRewardEntitlement>;
|
||||
}) => {
|
||||
const updated = [...entitlements];
|
||||
updated[index] = { ...updated[index], ...updates };
|
||||
setReward({ ...reward, featureGrantEntitlements: updated });
|
||||
};
|
||||
|
||||
const addEntitlement = () => {
|
||||
// Infer expiry from the first entitlement that has one
|
||||
const existingExpiry = entitlements.find((e) => e.expiry)?.expiry;
|
||||
setReward({
|
||||
...reward,
|
||||
featureGrantEntitlements: [
|
||||
...entitlements,
|
||||
{
|
||||
feature_id: "",
|
||||
allowance: 0,
|
||||
expiry: existingExpiry ? { ...existingExpiry } : undefined,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const removeEntitlement = ({ index }: { index: number }) => {
|
||||
setReward({
|
||||
...reward,
|
||||
featureGrantEntitlements: entitlements.filter((_, i) => i !== index),
|
||||
});
|
||||
};
|
||||
|
||||
const updatePromoCode = ({
|
||||
index,
|
||||
code,
|
||||
}: {
|
||||
index: number;
|
||||
code: string;
|
||||
}) => {
|
||||
const updated = [...(reward.promo_codes || [])];
|
||||
updated[index] = { ...updated[index], code };
|
||||
setReward({ ...reward, promo_codes: updated });
|
||||
};
|
||||
|
||||
const updateMaxRedemptions = ({
|
||||
index,
|
||||
value,
|
||||
}: {
|
||||
index: number;
|
||||
value: number | undefined;
|
||||
}) => {
|
||||
const updated = [...(reward.promo_codes || [])];
|
||||
updated[index] = { ...updated[index], max_redemptions: value };
|
||||
setReward({ ...reward, promo_codes: updated });
|
||||
};
|
||||
|
||||
const addPromoCode = () => {
|
||||
// Infer max_redemptions from first promo code
|
||||
const existingMax = reward.promo_codes?.find(
|
||||
(pc) => pc.max_redemptions,
|
||||
)?.max_redemptions;
|
||||
setReward({
|
||||
...reward,
|
||||
promo_codes: [
|
||||
...(reward.promo_codes || []),
|
||||
{ code: "", max_redemptions: existingMax },
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const removePromoCode = ({ index }: { index: number }) => {
|
||||
setReward({
|
||||
...reward,
|
||||
promo_codes: (reward.promo_codes || []).filter((_, i) => i !== index),
|
||||
});
|
||||
};
|
||||
|
||||
// Exclude features already selected in other entitlements
|
||||
const getAvailableFeatures = ({ currentIndex }: { currentIndex: number }) => {
|
||||
const selectedIds = entitlements
|
||||
.filter((_, i) => i !== currentIndex)
|
||||
.map((e) => e.feature_id);
|
||||
return meteredFeatures.filter((f) => !selectedIds.includes(f.id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Promo Codes Section */}
|
||||
<SheetSection title="Promo Codes">
|
||||
<div className="space-y-3">
|
||||
{(reward.promo_codes || []).map((promoCode, index) => (
|
||||
<div key={index} className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
{index === 0 && <FormLabel>Code</FormLabel>}
|
||||
<Input
|
||||
value={promoCode.code}
|
||||
onChange={(e) =>
|
||||
updatePromoCode({
|
||||
index,
|
||||
code: e.target.value
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]/g, ""),
|
||||
})
|
||||
}
|
||||
placeholder="PROMO2024"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
{index === 0 && <FormLabel>Max Uses</FormLabel>}
|
||||
<Input
|
||||
type="number"
|
||||
value={promoCode.max_redemptions ?? ""}
|
||||
onChange={(e) =>
|
||||
updateMaxRedemptions({
|
||||
index,
|
||||
value: e.target.value
|
||||
? Number(e.target.value)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
placeholder="Unlimited"
|
||||
/>
|
||||
</div>
|
||||
{(reward.promo_codes || []).length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removePromoCode({ index })}
|
||||
className="p-2 text-t4 hover:text-t1 transition-colors"
|
||||
>
|
||||
<TrashIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button variant="secondary" size="sm" onClick={addPromoCode}>
|
||||
<PlusIcon size={12} className="mr-1" />
|
||||
Add Code
|
||||
</Button>
|
||||
</div>
|
||||
</SheetSection>
|
||||
|
||||
{/* Entitlements Section */}
|
||||
<SheetSection title="Feature Grants">
|
||||
<div className="space-y-4">
|
||||
{entitlements.map((ent, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative space-y-3 rounded-lg border border-border p-3"
|
||||
>
|
||||
{entitlements.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeEntitlement({ index })}
|
||||
className="absolute top-2 right-2 p-1 text-t4 hover:text-t1 transition-colors cursor-pointer"
|
||||
>
|
||||
<TrashIcon size={12} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Feature selector */}
|
||||
<div>
|
||||
<FormLabel>Feature</FormLabel>
|
||||
<SearchableSelect<Feature>
|
||||
value={ent.feature_id || null}
|
||||
onValueChange={(value) =>
|
||||
updateEntitlement({
|
||||
index,
|
||||
updates: { feature_id: value },
|
||||
})
|
||||
}
|
||||
options={getAvailableFeatures({
|
||||
currentIndex: index,
|
||||
})}
|
||||
getOptionValue={(f) => f.id}
|
||||
getOptionLabel={(f) => f.name}
|
||||
placeholder="Select a metered feature..."
|
||||
searchable
|
||||
searchPlaceholder="Search features..."
|
||||
emptyText="No metered features found"
|
||||
triggerClassName="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Allowance */}
|
||||
<div>
|
||||
<FormLabel>Balance Grant</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
value={ent.allowance || ""}
|
||||
onChange={(e) =>
|
||||
updateEntitlement({
|
||||
index,
|
||||
updates: {
|
||||
allowance: Number(e.target.value),
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Expiry */}
|
||||
<div>
|
||||
<FormLabel>Expiry</FormLabel>
|
||||
{ent.expiry ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={ent.expiry.length || ""}
|
||||
onChange={(e) =>
|
||||
updateEntitlement({
|
||||
index,
|
||||
updates: {
|
||||
expiry: {
|
||||
duration:
|
||||
ent.expiry?.duration ??
|
||||
FeatureGrantDuration.Month,
|
||||
length: Number(e.target.value),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="30"
|
||||
className="w-20"
|
||||
/>
|
||||
<Select
|
||||
value={ent.expiry.duration}
|
||||
onValueChange={(value) =>
|
||||
updateEntitlement({
|
||||
index,
|
||||
updates: {
|
||||
expiry: {
|
||||
duration: value as FeatureGrantDuration,
|
||||
length: ent.expiry?.length ?? 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FeatureGrantDuration.Day}>
|
||||
Day(s)
|
||||
</SelectItem>
|
||||
<SelectItem value={FeatureGrantDuration.Week}>
|
||||
Week(s)
|
||||
</SelectItem>
|
||||
<SelectItem value={FeatureGrantDuration.Month}>
|
||||
Month(s)
|
||||
</SelectItem>
|
||||
<SelectItem value={FeatureGrantDuration.Year}>
|
||||
Year(s)
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateEntitlement({
|
||||
index,
|
||||
updates: { expiry: undefined },
|
||||
})
|
||||
}
|
||||
className="text-xs text-t4 hover:text-t1 transition-colors whitespace-nowrap"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateEntitlement({
|
||||
index,
|
||||
updates: {
|
||||
expiry: {
|
||||
duration: FeatureGrantDuration.Month,
|
||||
length: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
className="text-xs text-t4 hover:text-t1 transition-colors cursor-pointer"
|
||||
>
|
||||
No expiry (permanent). Click to set one.
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button variant="secondary" size="sm" onClick={addEntitlement}>
|
||||
<PlusIcon size={12} className="mr-1" />
|
||||
Add Entitlement
|
||||
</Button>
|
||||
</div>
|
||||
</SheetSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GiftIcon, PercentIcon } from "@phosphor-icons/react";
|
||||
import { LightningIcon, PercentIcon } from "@phosphor-icons/react";
|
||||
import { PanelButton } from "@/components/v2/buttons/PanelButton";
|
||||
import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import {
|
||||
@@ -23,13 +23,12 @@ export function SelectRewardType({ reward, setReward }: SelectRewardTypeProps) {
|
||||
onClick={() =>
|
||||
setReward({
|
||||
...reward,
|
||||
|
||||
rewardCategory: FrontendRewardCategory.Discount,
|
||||
discountType: FrontendDiscountType.Percentage,
|
||||
|
||||
discount_config: defaultDiscountConfig,
|
||||
free_product_id: null,
|
||||
free_product_config: null,
|
||||
featureGrantEntitlements: [],
|
||||
})
|
||||
}
|
||||
icon={<PercentIcon size={16} color="currentColor" />}
|
||||
@@ -45,23 +44,25 @@ export function SelectRewardType({ reward, setReward }: SelectRewardTypeProps) {
|
||||
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<PanelButton
|
||||
isSelected={reward.rewardCategory === "free_product"}
|
||||
isSelected={reward.rewardCategory === "feature_grant"}
|
||||
onClick={() =>
|
||||
setReward({
|
||||
...reward,
|
||||
rewardCategory: FrontendRewardCategory.FreeProduct,
|
||||
rewardCategory: FrontendRewardCategory.FeatureGrant,
|
||||
discountType: null,
|
||||
discount_config: null,
|
||||
free_product_id: null,
|
||||
free_product_config: null,
|
||||
featureGrantEntitlements: [{ feature_id: "", allowance: 0 }],
|
||||
})
|
||||
}
|
||||
icon={<GiftIcon size={16} color="currentColor" />}
|
||||
icon={<LightningIcon size={16} color="currentColor" />}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="text-body-highlight mb-1">Free Product</div>
|
||||
<div className="text-body-highlight mb-1">Feature Grant</div>
|
||||
<div className="text-body-secondary leading-tight">
|
||||
Used to give away products in a referral program
|
||||
Give your users a metered feature balance grant upon promo code
|
||||
redemption
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SheetHeader,
|
||||
} from "@/components/v2/sheets/SharedSheetComponents";
|
||||
import { Sheet, SheetContent } from "@/components/v2/sheets/Sheet";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
|
||||
import { useRewardStore } from "@/hooks/stores/useRewardStore";
|
||||
import { RewardService } from "@/services/products/RewardService";
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
mapFrontendToApiReward,
|
||||
} from "../../utils/rewardMappers";
|
||||
import { DiscountRewardConfig } from "./DiscountRewardConfig";
|
||||
import { FeatureGrantRewardConfig } from "./FeatureGrantRewardConfig";
|
||||
import { FreeProductRewardConfig } from "./FreeProductRewardConfig";
|
||||
import { RewardDetails } from "./RewardDetails";
|
||||
import { SelectRewardType } from "./SelectRewardType";
|
||||
@@ -35,6 +37,7 @@ export function UpdateRewardSheet({
|
||||
}: UpdateRewardSheetProps) {
|
||||
const axiosInstance = useAxiosInstance();
|
||||
const { refetch } = useRewardsQuery();
|
||||
const { features } = useFeaturesQuery();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -45,7 +48,10 @@ export function UpdateRewardSheet({
|
||||
// Initialize reward store when selectedReward changes
|
||||
useEffect(() => {
|
||||
if (open && selectedReward) {
|
||||
const frontendReward = mapApiToFrontendReward(selectedReward);
|
||||
const frontendReward = mapApiToFrontendReward({
|
||||
apiReward: selectedReward,
|
||||
features,
|
||||
});
|
||||
|
||||
setReward(frontendReward);
|
||||
setBaseReward(frontendReward);
|
||||
@@ -89,7 +95,10 @@ export function UpdateRewardSheet({
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const apiReward = mapFrontendToApiReward(reward);
|
||||
const apiReward = mapFrontendToApiReward({
|
||||
frontendReward: reward,
|
||||
features,
|
||||
});
|
||||
|
||||
await RewardService.updateReward({
|
||||
axiosInstance,
|
||||
@@ -132,6 +141,10 @@ export function UpdateRewardSheet({
|
||||
{reward.rewardCategory === "free_product" && (
|
||||
<FreeProductRewardConfig reward={reward} setReward={setReward} />
|
||||
)}
|
||||
|
||||
{reward.rewardCategory === "feature_grant" && (
|
||||
<FeatureGrantRewardConfig reward={reward} setReward={setReward} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SheetFooter>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CreateReward } from "@autumn/shared";
|
||||
import type { CreateReward, FeatureGrantDuration } from "@autumn/shared";
|
||||
|
||||
/**
|
||||
* Frontend-only reward category to separate UI concerns from API types
|
||||
@@ -6,6 +6,7 @@ import type { CreateReward } from "@autumn/shared";
|
||||
export enum FrontendRewardCategory {
|
||||
Discount = "discount",
|
||||
FreeProduct = "free_product",
|
||||
FeatureGrant = "feature_grant",
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,6 +18,16 @@ export enum FrontendDiscountType {
|
||||
InvoiceCredits = "invoice_credits",
|
||||
}
|
||||
|
||||
/** Frontend entitlement config for feature grant rewards */
|
||||
export interface FrontendRewardEntitlement {
|
||||
feature_id: string;
|
||||
allowance: number;
|
||||
expiry?: {
|
||||
duration: FeatureGrantDuration;
|
||||
length: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended reward type for frontend with separated concerns
|
||||
*/
|
||||
@@ -24,4 +35,6 @@ export interface FrontendReward extends Omit<CreateReward, "type"> {
|
||||
// Frontend-specific fields
|
||||
rewardCategory: FrontendRewardCategory | null;
|
||||
discountType: FrontendDiscountType | null;
|
||||
// Feature grant entitlements (frontend uses feature_id, mapped to internal_feature_id on submit)
|
||||
featureGrantEntitlements: FrontendRewardEntitlement[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type CreateReward, RewardType } from "@autumn/shared";
|
||||
import { type CreateReward, type Feature, RewardType } from "@autumn/shared";
|
||||
import type {
|
||||
FrontendDiscountType,
|
||||
FrontendReward,
|
||||
@@ -8,15 +8,26 @@ import type {
|
||||
/**
|
||||
* Maps frontend reward to API reward type
|
||||
*/
|
||||
export function mapFrontendToApiReward(
|
||||
frontendReward: FrontendReward,
|
||||
): CreateReward {
|
||||
const { rewardCategory, discountType, ...baseReward } = frontendReward;
|
||||
export function mapFrontendToApiReward({
|
||||
frontendReward,
|
||||
features,
|
||||
}: {
|
||||
frontendReward: FrontendReward;
|
||||
features?: Feature[];
|
||||
}): CreateReward {
|
||||
const {
|
||||
rewardCategory,
|
||||
discountType,
|
||||
featureGrantEntitlements,
|
||||
...baseReward
|
||||
} = frontendReward;
|
||||
|
||||
// Determine the API reward type based on frontend category and discount type
|
||||
let type: RewardType;
|
||||
|
||||
if (rewardCategory === "free_product") {
|
||||
if (rewardCategory === "feature_grant") {
|
||||
type = RewardType.FeatureGrant;
|
||||
} else if (rewardCategory === "free_product") {
|
||||
type = RewardType.FreeProduct;
|
||||
} else if (discountType === "percentage") {
|
||||
type = RewardType.PercentageDiscount;
|
||||
@@ -25,26 +36,47 @@ export function mapFrontendToApiReward(
|
||||
} else if (discountType === "invoice_credits") {
|
||||
type = RewardType.InvoiceCredits;
|
||||
} else {
|
||||
// Default fallback
|
||||
type = RewardType.PercentageDiscount;
|
||||
}
|
||||
|
||||
return {
|
||||
const result: CreateReward = {
|
||||
...baseReward,
|
||||
type,
|
||||
};
|
||||
|
||||
// Map frontend feature_id → internal_feature_id for feature grant entitlements
|
||||
if (rewardCategory === "feature_grant" && featureGrantEntitlements?.length) {
|
||||
result.entitlements = featureGrantEntitlements
|
||||
.filter((e) => e.feature_id && e.allowance > 0)
|
||||
.map((e) => {
|
||||
const feature = features?.find((f) => f.id === e.feature_id);
|
||||
return {
|
||||
internal_feature_id: feature?.internal_id ?? e.feature_id,
|
||||
allowance: e.allowance,
|
||||
expiry: e.expiry,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps API reward to frontend reward
|
||||
*/
|
||||
export function mapApiToFrontendReward(
|
||||
apiReward: CreateReward,
|
||||
): FrontendReward {
|
||||
export function mapApiToFrontendReward({
|
||||
apiReward,
|
||||
features,
|
||||
}: {
|
||||
apiReward: CreateReward;
|
||||
features?: Feature[];
|
||||
}): FrontendReward {
|
||||
let rewardCategory: FrontendRewardCategory | null = null;
|
||||
let discountType: FrontendDiscountType | null = null;
|
||||
|
||||
if (apiReward.type === RewardType.FreeProduct) {
|
||||
if (apiReward.type === RewardType.FeatureGrant) {
|
||||
rewardCategory = "feature_grant";
|
||||
} else if (apiReward.type === RewardType.FreeProduct) {
|
||||
rewardCategory = "free_product";
|
||||
} else {
|
||||
rewardCategory = "discount";
|
||||
@@ -57,11 +89,24 @@ export function mapApiToFrontendReward(
|
||||
}
|
||||
}
|
||||
|
||||
const { type, ...baseReward } = apiReward;
|
||||
const { type, entitlements, ...baseReward } = apiReward;
|
||||
|
||||
// Map internal_feature_id → feature_id for display
|
||||
const featureGrantEntitlements = (entitlements ?? []).map((e) => {
|
||||
const feature = features?.find(
|
||||
(f) => f.internal_id === e.internal_feature_id,
|
||||
);
|
||||
return {
|
||||
feature_id: feature?.id ?? e.internal_feature_id,
|
||||
allowance: e.allowance,
|
||||
expiry: e.expiry,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...baseReward,
|
||||
rewardCategory,
|
||||
discountType,
|
||||
featureGrantEntitlements,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user