Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
678202c
refactor: dialog container
ogzhanolguncu Apr 18, 2025
1d75f87
feat: add layout
ogzhanolguncu Apr 18, 2025
896560d
feat: add general setup form
ogzhanolguncu Apr 18, 2025
3e1155b
feat: add step validation
ogzhanolguncu Apr 18, 2025
1a353da
feat: add ratelimit step
ogzhanolguncu Apr 18, 2025
20cde15
feat: add usage limit
ogzhanolguncu Apr 18, 2025
096b3ee
feat: expiration
ogzhanolguncu Apr 18, 2025
622aa58
chore: run fmt
ogzhanolguncu Apr 18, 2025
c4ef986
feat: add metadata
ogzhanolguncu Apr 18, 2025
4c0ef46
fix: schema structure
ogzhanolguncu Apr 18, 2025
be44c3d
refactor: remove unused codes
ogzhanolguncu Apr 18, 2025
011589c
refactor: form names
ogzhanolguncu Apr 18, 2025
b9192f3
refactor: make dialog component more composable
ogzhanolguncu Apr 21, 2025
4936df0
refactor: persist form for accidental closes
ogzhanolguncu Apr 21, 2025
0d1edb3
fix: ref issue
ogzhanolguncu Apr 21, 2025
746d500
fix: add validation after loading persisted state
ogzhanolguncu Apr 21, 2025
ce08839
feat: add key submit
ogzhanolguncu Apr 21, 2025
1e1b617
refactor: move validation logic to hook
ogzhanolguncu Apr 21, 2025
f488658
feat: add final create key screen
ogzhanolguncu Apr 21, 2025
454b216
fix: wrong payload
ogzhanolguncu Apr 22, 2025
a7c77e5
fix: undefined issues
ogzhanolguncu Apr 22, 2025
1dc2f70
fix: credits structure
ogzhanolguncu Apr 22, 2025
30ea84b
Merge branch 'main' into keys-create-menu
ogzhanolguncu Apr 22, 2025
94b409a
fix: type
ogzhanolguncu Apr 22, 2025
5b5fe59
fix: coderabbit isues
ogzhanolguncu Apr 22, 2025
c219dae
fix: coderabbit fix
ogzhanolguncu Apr 22, 2025
b591fc3
feat: add final confirmation
ogzhanolguncu Apr 22, 2025
4ba872f
fix: coderabbit issue
ogzhanolguncu Apr 22, 2025
496c12e
fix: move persisted hook to global
ogzhanolguncu Apr 25, 2025
f5bbb2a
feat: add multiple ratelimits to setup
ogzhanolguncu Apr 25, 2025
4bf7030
refactor: improve error handling
ogzhanolguncu Apr 25, 2025
a37ccfb
fix: style issues
ogzhanolguncu Apr 25, 2025
ceda977
Merge branch 'main' of github.com:unkeyed/unkey into keys-create-menu
ogzhanolguncu Apr 25, 2025
241b58d
chore: run fmt
ogzhanolguncu Apr 25, 2025
284a7e7
fix: add ratelimit creation
ogzhanolguncu Apr 28, 2025
9346cc4
feat: make the schema SSOT for key creation
ogzhanolguncu Apr 28, 2025
8142cb0
refactor: add proper namespacing
ogzhanolguncu Apr 28, 2025
d3ff0ec
fix: vercel comments
ogzhanolguncu Apr 28, 2025
5f5e91c
fix: replace ownerId with external ID
ogzhanolguncu Apr 28, 2025
ae7d42c
fix: base ui components stylings
ogzhanolguncu Apr 28, 2025
c02ae4d
fix: json field set
ogzhanolguncu Apr 28, 2025
2032a5a
fix: create another button
ogzhanolguncu Apr 28, 2025
a91897d
Merge branch 'main' into keys-create-menu
ogzhanolguncu Apr 28, 2025
14c511a
fix: expiration issues
ogzhanolguncu Apr 28, 2025
594e8a5
fix: expiration issues
ogzhanolguncu Apr 28, 2025
f91fe79
fix: type issue
ogzhanolguncu Apr 28, 2025
c3690b7
fix: use 10 minute as our buffer expiration
ogzhanolguncu Apr 28, 2025
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
@@ -0,0 +1,180 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ChartPie, CircleInfo } from "@unkey/icons";
import { FormInput } from "@unkey/ui";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { CreditsFormValues } from "../create-key.schema";
import { ProtectionSwitch } from "./protection-switch";

export const UsageSetup = () => {
const {
register,
formState: { errors },
control,
setValue,
getValues,
trigger,
} = useFormContext<CreditsFormValues>();

const limitEnabled = useWatch({
control,
name: "limit.enabled",
});

const currentRefillInterval = useWatch({
control,
name: "limit.data.refill.interval",
});

const handleSwitchChange = (checked: boolean) => {
setValue("limit.enabled", checked);

// When enabling, ensure refill has the correct structure
if (checked && !getValues("limit.data.refill.interval")) {
setValue("limit.data.refill.interval", "none", { shouldValidate: true });
setValue("limit.data.refill.amount", undefined, { shouldValidate: true });
setValue("limit.data.refill.refillDay", undefined, {
shouldValidate: true,
});
}

trigger("limit");
};

const handleRefillIntervalChange = (value: "none" | "daily" | "monthly") => {
setValue("limit.data.refill.interval", value, { shouldValidate: true });

// Clean up related fields based on the selected interval
if (value === "none") {
setValue("limit.data.refill.amount", undefined, { shouldValidate: true });
setValue("limit.data.refill.refillDay", undefined, {
shouldValidate: true,
});
} else if (value === "daily") {
// For daily, ensure refillDay is undefined but keep amount if present
setValue("limit.data.refill.refillDay", undefined, {
shouldValidate: true,
});
// If amount is not set, set a default
if (!getValues("limit.data.refill.amount")) {
setValue("limit.data.refill.amount", 100, { shouldValidate: true });
}
} else if (value === "monthly") {
// For monthly, ensure both amount and refillDay have values
if (!getValues("limit.data.refill.amount")) {
setValue("limit.data.refill.amount", 100, { shouldValidate: true });
}
if (!getValues("limit.data.refill.refillDay")) {
setValue("limit.data.refill.refillDay", 1, { shouldValidate: true });
}
}
};

return (
<div className="space-y-5 px-2 py-1">
<ProtectionSwitch
description="Turn on to limit how many times this key can be used. Once the limit
is reached, the key will be disabled."
title="Credits"
icon={<ChartPie className="text-gray-12" size="sm-regular" />}
checked={limitEnabled}
onCheckedChange={handleSwitchChange}
{...register("limit.enabled")}
/>
<FormInput
className="[&_input:first-of-type]:h-[36px]"
placeholder="100"
inputMode="numeric"
type="number"
label="Number of uses"
description="Enter the remaining amount of uses for this key."
error={errors.limit?.data?.remaining?.message}
disabled={!limitEnabled}
readOnly={!limitEnabled}
{...register("limit.data.remaining")}
/>

<Controller
control={control}
name="limit.data.refill.interval"
render={({ field }) => (
<div className="space-y-1.5">
<div className="text-gray-11 text-[13px] flex items-center">Refill Rate</div>
<Select
onValueChange={(value) => {
handleRefillIntervalChange(value as "none" | "daily" | "monthly");
}}
value={field.value || "none"}
disabled={!limitEnabled}
>
<SelectTrigger className="h-9">
<SelectValue placeholder="Select refill interval" />
</SelectTrigger>
<SelectContent className="border-none rounded-md">
<SelectItem value="none">None</SelectItem>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="monthly">Monthly</SelectItem>
</SelectContent>
</Select>
<output className="text-gray-9 flex gap-2 items-center text-[13px]">
<CircleInfo size="md-regular" aria-hidden="true" />
<span>Interval key will be refilled.</span>
</output>
</div>
)}
/>

<Controller
control={control}
name="limit.data.refill.amount"
render={({ field }) => (
<FormInput
className="[&_input:first-of-type]:h-[36px]"
placeholder="100"
inputMode="numeric"
type="number"
label="Number of uses per interval"
description="Enter the number of uses to refill per interval."
error={errors.limit?.data?.refill?.amount?.message}
disabled={!limitEnabled || currentRefillInterval === "none"}
readOnly={!limitEnabled || currentRefillInterval === "none"}
value={field.value === undefined ? "" : field.value}
onChange={(e) => {
const value = e.target.value === "" ? undefined : Number(e.target.value);
field.onChange(value);
}}
/>
)}
/>

<Controller
control={control}
name="limit.data.refill.refillDay"
render={({ field }) => (
<FormInput
className="[&_input:first-of-type]:h-[36px]"
placeholder="1"
inputMode="numeric"
type="number"
label="On which day of the month should we refill the key?"
description="Enter the day to refill monthly (1-31)."
error={errors.limit?.data?.refill?.refillDay?.message}
disabled={!limitEnabled || currentRefillInterval !== "monthly"}
readOnly={!limitEnabled || currentRefillInterval !== "monthly"}
value={field.value === undefined ? "" : field.value}
onChange={(e) => {
const value = e.target.value === "" ? undefined : Number(e.target.value);
field.onChange(value);
}}
/>
)}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"use client";
import { DatetimePopover } from "@/components/logs/datetime/datetime-popover";
import { Clock } from "@unkey/icons";
import { FormInput } from "@unkey/ui";
import { addDays, addMinutes, format } from "date-fns";
import { useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { ExpirationFormValues } from "../create-key.schema";
import { ProtectionSwitch } from "./protection-switch";

const EXPIRATION_OPTIONS = [
{
id: 1,
display: "1 day",
value: "1d",
description: "Key expires in 1 day",
checked: false,
},
{
id: 2,
display: "1 week",
value: "7d",
description: "Key expires in 1 week",
checked: false,
},
{
id: 3,
display: "1 month",
value: "30d",
description: "Key expires in 30 days",
checked: false,
},
{
id: 4,
display: "Custom",
value: undefined,
description: "Set custom expiration date and time",
checked: false,
},
];

export const ExpirationSetup = () => {
const {
register,
formState: { errors },
control,
setValue,
} = useFormContext<ExpirationFormValues>();

const [selectedTitle, setSelectedTitle] = useState<string>("1 day");

const expirationEnabled = useWatch({
control,
name: "expiration.enabled",
});

const currentExpiryDate = useWatch({
control,
name: "expiration.data",
});

const handleSwitchChange = (checked: boolean) => {
setValue("expiration.enabled", checked);

// Set default expiry date (1 day) when enabling if not already set
if (checked && !currentExpiryDate) {
setValue("expiration.data", addDays(new Date(), 1));
}
};

// Calculate minimum valid date (10 minutes from now)
const minValidDate = addMinutes(new Date(), 10);

// Handle date and time selection from DatetimePopover
const handleDateTimeChange = (startTime?: number, _?: number, since?: string) => {
if (since) {
// Handle predefined time ranges
let newDate = new Date();
switch (since) {
case "1d":
newDate = addDays(newDate, 1);
break;
case "7d":
newDate = addDays(newDate, 7);
break;
case "30d":
newDate = addDays(newDate, 30);
break;
}
setValue("expiration.data", newDate);
} else if (startTime) {
// Handle custom date selection
const newDate = new Date(startTime);

// Check if the date is valid (at least 2 minutes in the future)
if (newDate < minValidDate) {
// If date is too soon, set it to minimum valid date
setValue("expiration.data", minValidDate);
} else {
setValue("expiration.data", newDate);
}
}
};

// Format date for display
const formatExpiryDate = (date?: Date) => {
if (!date) {
return "Select expiration date";
}
return format(date, "MMM d, yyyy 'at' h:mm a");
};

const getInitialTimeValues = () => {
// If we have a current expiry date, use it, otherwise use minimum valid date
const initialDate = currentExpiryDate || minValidDate;

return {
startTime: initialDate.getTime(),
endTime: undefined, // Not needed for single date mode
since: undefined,
};
};

// Calculate date for showing warning about close expiry (less than 1 hour)
const isExpiringVerySoon =
currentExpiryDate &&
new Date(currentExpiryDate).getTime() - new Date().getTime() < 60 * 60 * 1000;

const getExpiryDescription = () => {
if (isExpiringVerySoon) {
return "This key will expire very soon (less than 1 hour). Consider setting a longer expiration time.";
}
return "The key will be automatically disabled at the specified date and time (UTC).";
};
Comment on lines +125 to +134
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix expiration warning when expiration is disabled

Per PR comments, an expiration warning appears even when the date field is disabled. This is because isExpiringVerySoon can be true even when expirationEnabled is false.

// Calculate date for showing warning about close expiry (less than 1 hour)
const isExpiringVerySoon =
- currentExpiryDate &&
+ expirationEnabled && currentExpiryDate &&
  new Date(currentExpiryDate).getTime() - new Date().getTime() < 60 * 60 * 1000;

const getExpiryDescription = () => {
  if (isExpiringVerySoon) {
    return "This key will expire very soon (less than 1 hour). Consider setting a longer expiration time.";
  }
  return "The key will be automatically disabled at the specified date and time (UTC).";
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isExpiringVerySoon =
currentExpiryDate &&
new Date(currentExpiryDate).getTime() - new Date().getTime() < 60 * 60 * 1000;
const getExpiryDescription = () => {
if (isExpiringVerySoon) {
return "This key will expire very soon (less than 1 hour). Consider setting a longer expiration time.";
}
return "The key will be automatically disabled at the specified date and time (UTC).";
};
const isExpiringVerySoon =
expirationEnabled && currentExpiryDate &&
new Date(currentExpiryDate).getTime() - new Date().getTime() < 60 * 60 * 1000;
const getExpiryDescription = () => {
if (isExpiringVerySoon) {
return "This key will expire very soon (less than 1 hour). Consider setting a longer expiration time.";
}
return "The key will be automatically disabled at the specified date and time (UTC).";
};


return (
<div className="space-y-5 px-2 py-1">
<ProtectionSwitch
description="Turn on to set an expiration date. When reached, the key will be automatically disabled."
title="Expiration"
icon={<Clock className="text-gray-12" size="sm-regular" />}
checked={expirationEnabled}
onCheckedChange={handleSwitchChange}
{...register("expiration.enabled")}
/>

<Controller
control={control}
name="expiration.data"
render={({ field }) => (
<DatetimePopover
initialTitle={selectedTitle}
initialTimeValues={getInitialTimeValues()}
onSuggestionChange={setSelectedTitle}
onDateTimeChange={handleDateTimeChange}
customOptions={EXPIRATION_OPTIONS}
customHeader={<ExpirationHeader />}
singleDateMode
minDate={minValidDate} // Set minimum date to 2 minutes from now
>
<FormInput
label="Expiry Date"
description={getExpiryDescription()}
readOnly
disabled={!expirationEnabled}
value={formatExpiryDate(field.value)}
className="cursor-pointer w-full"
variant={expirationEnabled && isExpiringVerySoon ? "warning" : undefined}
error={errors.expiration?.data?.message}
/>
</DatetimePopover>
)}
/>
Comment on lines +147 to +173
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Validate and clear form errors correctly

Per PR comments, the date field is automatically marked as invalid and selecting a valid date doesn't clear the validation error. This may be related to how form validation is handled.

Check if the form validation is properly updated when dates change. Consider logging the validation in the handleDateTimeChange function:


🏁 Script executed:

#!/bin/bash
# Search for form validation schema for expiration
rg -A 10 -B 10 "expiration.*schema|schema.*expiration" --glob="*.{ts,tsx}"

Length of output: 76


🏁 Script executed:

#!/bin/bash
# Locate where handleDateTimeChange is defined/used
rg -n "handleDateTimeChange" -A 5 -B 5 --glob="*.{ts,tsx}"

# Check if clearErrors is ever called in relation to expiration
rg -n "clearErrors" -A 5 -B 5 --glob="*.{ts,tsx}"

Length of output: 6363


🏁 Script executed:

#!/bin/bash
# Inspect handleDateTimeChange implementation in expiration-setup.tsx and related form methods
file="apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/expiration-setup.tsx"

# Show the function body
rg -n -A30 -B0 "const handleDateTimeChange" -- "$file"

# Check how the form state is updated
rg -n "setValue" -- "$file"

# Verify if errors are cleared or set
rg -n "clearErrors" -- "$file"
rg -n "setError" -- "$file"

Length of output: 1890


Clear or re-validate the expiry field when the date changes

The handleDateTimeChange helper updates expiration.data via setValue but never clears or re-validates the field, so any prior validation error remains. To fix this, update the logic in expiration-setup.tsx as follows:

• Destructure and call clearErrors("expiration.data") inside handleDateTimeChange immediately after you call setValue for a valid date.
• Alternatively, invoke setValue("expiration.data", newDate, { shouldValidate: true }) to re-trigger validation and clear errors automatically.
• You can also leverage the Controller’s field.onChange callback instead of setValue directly, so React Hook Form runs its built-in validation flow.

Locations to update:

  • apps/dashboard/app/(app)/apis/[apiId]/_components/create-key/components/expiration-setup.tsx
    handleDateTimeChange (lines 75–103)

</div>
);
};

const ExpirationHeader = () => {
return (
<div className="flex justify-between w-full h-8 px-2">
<span className="text-gray-9 text-[13px] w-full">Choose expiration date</span>
</div>
);
};
Loading
Loading