import { useEffect, useState } from "react" import { Plus } from "lucide-react" import { toast } from "sonner" import { createAppVersion } from "../../../api/app-versions.api" import { Button } from "../../../components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../../components/ui/dialog" import { Input } from "../../../components/ui/input" import { Select } from "../../../components/ui/select" import { SpinnerIcon } from "../../../components/ui/spinner-icon" import { Textarea } from "../../../components/ui/textarea" import { APP_PLATFORMS, APP_UPDATE_TYPES, APP_VERSION_STATUSES, DEFAULT_STORE_URLS, } from "../../../lib/appVersions" import type { AppPlatform, AppUpdateType, AppVersion, AppVersionStatus, CreateAppVersionPayload, } from "../../../types/app-version.types" export interface CreateAppVersionDraft { platform: AppPlatform version_name: string version_code: string update_type: AppUpdateType release_notes: string store_url: string min_supported_version_code: string status: AppVersionStatus } export const EMPTY_APP_VERSION_DRAFT: CreateAppVersionDraft = { platform: "ANDROID", version_name: "", version_code: "", update_type: "FORCE", release_notes: "", store_url: DEFAULT_STORE_URLS.ANDROID, min_supported_version_code: "", status: "ACTIVE", } function draftToPayload(draft: CreateAppVersionDraft): CreateAppVersionPayload | null { const version_name = draft.version_name.trim() const version_code = Number(draft.version_code) const min_supported_version_code = Number(draft.min_supported_version_code) const release_notes = draft.release_notes.trim() const store_url = draft.store_url.trim() if (!version_name) return null if (!Number.isFinite(version_code) || version_code < 1) return null if (!Number.isFinite(min_supported_version_code) || min_supported_version_code < 0) return null if (!release_notes) return null if (!store_url) return null try { new URL(store_url) } catch { return null } return { platform: draft.platform, version_name, version_code, update_type: draft.update_type, release_notes, store_url, min_supported_version_code, status: draft.status, } } type CreateAppVersionDialogProps = { open: boolean onOpenChange: (open: boolean) => void onCreated: (version: AppVersion) => void } export function CreateAppVersionDialog({ open, onOpenChange, onCreated, }: CreateAppVersionDialogProps) { const [draft, setDraft] = useState(EMPTY_APP_VERSION_DRAFT) const [saving, setSaving] = useState(false) useEffect(() => { if (!open) { setDraft(EMPTY_APP_VERSION_DRAFT) setSaving(false) } }, [open]) const handlePlatformChange = (platform: AppPlatform) => { setDraft((d) => ({ ...d, platform, store_url: d.store_url === DEFAULT_STORE_URLS.ANDROID || d.store_url === DEFAULT_STORE_URLS.IOS ? DEFAULT_STORE_URLS[platform] ?? d.store_url : d.store_url, })) } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() const payload = draftToPayload(draft) if (!payload) { toast.error("Please fill in all required fields with valid values.") return } setSaving(true) try { const res = await createAppVersion(payload) if (!res.data) { toast.error("Version was created but the response could not be read.") return } toast.success(res.message || "App version created successfully") onCreated(res.data) onOpenChange(false) } catch { toast.error("Failed to create app version.") } finally { setSaving(false) } } return (
New app version Publishes a release via{" "} POST /admin/app-versions . Learners on older builds will see update prompts based on these rules.
setDraft((d) => ({ ...d, version_name: e.target.value }))} placeholder="1.3.0" className="rounded-[6px]" required />
setDraft((d) => ({ ...d, version_code: e.target.value }))} placeholder="15" className="rounded-[6px]" required />
setDraft((d) => ({ ...d, min_supported_version_code: e.target.value })) } placeholder="12" className="rounded-[6px]" required />

Builds below this code are prompted to update

setDraft((d) => ({ ...d, store_url: e.target.value }))} placeholder="https://play.google.com/store/apps/details?id=…" className="rounded-[6px]" required />