feat(topic state and command) - add & delete topic
This commit is contained in:
@@ -4,4 +4,5 @@ export const ENV = {
|
|||||||
secretKey: import.meta.env.VITE_SIGN_SECRET_KEY,
|
secretKey: import.meta.env.VITE_SIGN_SECRET_KEY,
|
||||||
basicUsername: import.meta.env.VITE_BASIC_AUTH_USERNAME,
|
basicUsername: import.meta.env.VITE_BASIC_AUTH_USERNAME,
|
||||||
basicPassword: import.meta.env.VITE_BASIC_AUTH_PASSWORD,
|
basicPassword: import.meta.env.VITE_BASIC_AUTH_PASSWORD,
|
||||||
|
credential: import.meta.env.VITE_CREDENTIAL,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,9 +16,13 @@ export default function SettingsFeature() {
|
|||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
function isObjectValueDifferent(a: MerchantForm, b: MerchantForm): boolean {
|
function isObjectValueDifferent(a: MerchantForm, b: MerchantForm): boolean {
|
||||||
return (Object.keys(a) as (keyof MerchantForm)[]).some(
|
const keys: (keyof MerchantForm)[] = [
|
||||||
(key) => a[key] !== b[key],
|
"merchantName",
|
||||||
);
|
"tower",
|
||||||
|
"floor",
|
||||||
|
"unit",
|
||||||
|
];
|
||||||
|
return keys.some((key) => a[key] !== b[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
|||||||
24
src/features/topics/hooks/mutations.ts
Normal file
24
src/features/topics/hooks/mutations.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { createTopic, deleteTopic } from "../../../repositories/device";
|
||||||
|
|
||||||
|
export function useCreateTopic() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: TopicPayload) => createTopic(payload),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["topics/state"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["topics/command"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteTopic() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: TopicPayload) => deleteTopic(payload),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["topics/state"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["topics/command"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,15 +1,100 @@
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { cn } from "../../utils/classname";
|
import { cn } from "../../utils/classname";
|
||||||
import StateSection from "./sections/state";
|
import StateSection from "./sections/state";
|
||||||
import CommandSection from "./sections/command";
|
import CommandSection from "./sections/command";
|
||||||
|
import { useCreateTopic } from "./hooks/mutations";
|
||||||
|
import { getMerchant } from "../../utils/storage";
|
||||||
|
import { ENV } from "../../constants/env";
|
||||||
|
|
||||||
export default function TopicsFeature() {
|
export default function TopicsFeature() {
|
||||||
const [tab, setTab] = useState("state");
|
const [tab, setTab] = useState("state");
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [form, setForm] = useState<TopicPayload>({
|
||||||
|
topic: "",
|
||||||
|
type: "state-reply",
|
||||||
|
});
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const createTopic = useCreateTopic();
|
||||||
|
const merchant = getMerchant();
|
||||||
|
const expectedCredential = ENV.credential?.trim();
|
||||||
|
const hasCredential = Boolean(merchant?.credential?.trim());
|
||||||
|
const isCredentialMatch =
|
||||||
|
Boolean(expectedCredential) &&
|
||||||
|
merchant?.credential?.trim() === expectedCredential;
|
||||||
|
const isMerchantSetup =
|
||||||
|
Boolean(merchant?.merchantName?.trim()) &&
|
||||||
|
Boolean(merchant?.tower?.trim()) &&
|
||||||
|
Boolean(merchant?.floor?.trim()) &&
|
||||||
|
Boolean(merchant?.unit?.trim());
|
||||||
|
const isReady = isMerchantSetup && hasCredential && isCredentialMatch;
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setError("");
|
||||||
|
setForm({
|
||||||
|
topic: "",
|
||||||
|
type: tab === "state" ? "state-reply" : "commands",
|
||||||
|
});
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (createTopic.isPending) return;
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const topic = form.topic.trim();
|
||||||
|
if (!topic) {
|
||||||
|
setError("Topic wajib diisi.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createTopic.mutate(
|
||||||
|
{ topic, type: form.type },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setForm({
|
||||||
|
topic: "",
|
||||||
|
type: tab === "state" ? "state-reply" : "commands",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setError("Gagal menambah topic.");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
{!isReady && (
|
||||||
|
<div className="mx-4 rounded-md border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700">
|
||||||
|
{!isMerchantSetup && (
|
||||||
|
<span>
|
||||||
|
Silakan setup merchant terlebih dahulu di halaman Settings sebelum
|
||||||
|
mengakses Topics.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isMerchantSetup && !hasCredential && (
|
||||||
|
<span>
|
||||||
|
Silakan isi credential terlebih dahulu di halaman Settings sebelum
|
||||||
|
mengakses Topics.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isMerchantSetup && hasCredential && !isCredentialMatch && (
|
||||||
|
<span>Credential tidak sesuai.</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="p-4 flex justify-end">
|
<div className="p-4 flex justify-end">
|
||||||
<button className="shrink-0 px-4 py-2 rounded-md flex items-center justify-center transition-all bg-orange-500 text-white cursor-pointer">
|
<button
|
||||||
|
onClick={handleOpen}
|
||||||
|
className="shrink-0 px-4 py-2 rounded-md flex items-center justify-center transition-all bg-orange-500 text-white disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
disabled={!isReady}
|
||||||
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
Tambah Topic
|
Tambah Topic
|
||||||
</button>
|
</button>
|
||||||
@@ -36,6 +121,71 @@ export default function TopicsFeature() {
|
|||||||
</div>
|
</div>
|
||||||
{tab === "state" && <StateSection />}
|
{tab === "state" && <StateSection />}
|
||||||
{tab === "command" && <CommandSection />}
|
{tab === "command" && <CommandSection />}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
||||||
|
<div className="w-full max-w-md rounded-md bg-white p-4 shadow-lg">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold">Tambah Topic</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="text-sm text-neutral-500"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Tutup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleSubmit} className="mt-4 flex flex-col gap-3">
|
||||||
|
<label className="text-sm font-medium text-neutral-700">
|
||||||
|
Topic
|
||||||
|
<input
|
||||||
|
className="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm outline-none focus:border-amber-500"
|
||||||
|
placeholder="Masukkan topic"
|
||||||
|
value={form.topic}
|
||||||
|
onChange={(event) =>
|
||||||
|
setForm((prev) => ({ ...prev, topic: event.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="text-sm font-medium text-neutral-700">
|
||||||
|
Type
|
||||||
|
<select
|
||||||
|
className="mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm outline-none focus:border-amber-500"
|
||||||
|
value={form.type}
|
||||||
|
onChange={(event) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
type: event.target.value as TopicPayload["type"],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="state-reply">state-reply</option>
|
||||||
|
<option value="commands">commands</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-red-500">{error}</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-2 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="rounded-md border border-neutral-300 px-4 py-2 text-sm"
|
||||||
|
disabled={createTopic.isPending}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-md bg-amber-500 px-4 py-2 text-sm text-white disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled={createTopic.isPending}
|
||||||
|
>
|
||||||
|
{createTopic.isPending ? "Menyimpan..." : "Simpan"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,57 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { useTopicCommand } from "../hooks/queries";
|
import { useTopicCommand } from "../hooks/queries";
|
||||||
import { Edit, Trash2 } from "lucide-react";
|
import { Edit, Trash2 } from "lucide-react";
|
||||||
|
import { useDeleteTopic } from "../hooks/mutations";
|
||||||
|
|
||||||
|
type PendingDelete = {
|
||||||
|
open: boolean;
|
||||||
|
topic: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function CommandSection() {
|
export default function CommandSection() {
|
||||||
const { data } = useTopicCommand();
|
const { data, isLoading } = useTopicCommand();
|
||||||
|
const deleteTopic = useDeleteTopic();
|
||||||
|
const [pending, setPending] = useState<PendingDelete>({
|
||||||
|
open: false,
|
||||||
|
topic: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = (topic: string) => {
|
||||||
|
if (deleteTopic.isPending) return;
|
||||||
|
setPending({ open: true, topic });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (!pending.topic || deleteTopic.isPending) return;
|
||||||
|
deleteTopic.mutate(
|
||||||
|
{ topic: pending.topic, type: "commands" },
|
||||||
|
{
|
||||||
|
onSuccess: () => setPending({ open: false, topic: "" }),
|
||||||
|
onError: () => setPending({ open: false, topic: "" }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (deleteTopic.isPending) return;
|
||||||
|
setPending({ open: false, topic: "" });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{isLoading &&
|
||||||
|
Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={`loading-${index}`}
|
||||||
|
className="bg-white border-b border-neutral-200 p-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="h-4 w-40 animate-pulse rounded bg-neutral-200" />
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="h-4 w-4 animate-pulse rounded bg-neutral-200" />
|
||||||
|
<div className="h-4 w-4 animate-pulse rounded bg-neutral-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
{data?.map((item, key) => (
|
{data?.map((item, key) => (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
@@ -12,11 +59,47 @@ export default function CommandSection() {
|
|||||||
>
|
>
|
||||||
<h1>{item.topic}</h1>
|
<h1>{item.topic}</h1>
|
||||||
<div className="flex gap-2 ">
|
<div className="flex gap-2 ">
|
||||||
<Edit className="text-blue-500 cursor-pointer" size={18} />
|
<Edit className="text-neutral-300" size={18} />
|
||||||
<Trash2 className="text-red-500 cursor-pointer" size={18} />
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDelete(item.topic)}
|
||||||
|
className="text-red-500 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
disabled={deleteTopic.isPending}
|
||||||
|
aria-label={`Hapus topic ${item.topic}`}
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{pending.open && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
||||||
|
<div className="w-full max-w-sm rounded-md bg-white p-4 shadow-lg transition-all duration-200 ease-out">
|
||||||
|
<h3 className="text-base font-semibold">Hapus Topic</h3>
|
||||||
|
<p className="mt-2 text-sm text-neutral-600">
|
||||||
|
Yakin ingin menghapus topic "{pending.topic}"?
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="rounded-md border border-neutral-300 px-4 py-2 text-sm"
|
||||||
|
disabled={deleteTopic.isPending}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
className="rounded-md bg-red-500 px-4 py-2 text-sm text-white disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled={deleteTopic.isPending}
|
||||||
|
>
|
||||||
|
{deleteTopic.isPending ? "Menghapus..." : "Hapus"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,57 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { useTopics } from "../hooks/queries";
|
import { useTopics } from "../hooks/queries";
|
||||||
import { Edit, Trash2 } from "lucide-react";
|
import { Edit, Trash2 } from "lucide-react";
|
||||||
|
import { useDeleteTopic } from "../hooks/mutations";
|
||||||
|
|
||||||
|
type PendingDelete = {
|
||||||
|
open: boolean;
|
||||||
|
topic: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function StateSection() {
|
export default function StateSection() {
|
||||||
const { data } = useTopics();
|
const { data, isLoading } = useTopics();
|
||||||
|
const deleteTopic = useDeleteTopic();
|
||||||
|
const [pending, setPending] = useState<PendingDelete>({
|
||||||
|
open: false,
|
||||||
|
topic: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = (topic: string) => {
|
||||||
|
if (deleteTopic.isPending) return;
|
||||||
|
setPending({ open: true, topic });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (!pending.topic || deleteTopic.isPending) return;
|
||||||
|
deleteTopic.mutate(
|
||||||
|
{ topic: pending.topic, type: "state-reply" },
|
||||||
|
{
|
||||||
|
onSuccess: () => setPending({ open: false, topic: "" }),
|
||||||
|
onError: () => setPending({ open: false, topic: "" }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (deleteTopic.isPending) return;
|
||||||
|
setPending({ open: false, topic: "" });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{isLoading &&
|
||||||
|
Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={`loading-${index}`}
|
||||||
|
className="bg-white border-b border-neutral-200 p-4 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="h-4 w-40 animate-pulse rounded bg-neutral-200" />
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="h-4 w-4 animate-pulse rounded bg-neutral-200" />
|
||||||
|
<div className="h-4 w-4 animate-pulse rounded bg-neutral-200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
{data?.map((item, key) => (
|
{data?.map((item, key) => (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
@@ -12,11 +59,47 @@ export default function StateSection() {
|
|||||||
>
|
>
|
||||||
<h1>{item.topic}</h1>
|
<h1>{item.topic}</h1>
|
||||||
<div className="flex gap-2 ">
|
<div className="flex gap-2 ">
|
||||||
<Edit className="text-blue-500 cursor-pointer" size={18} />
|
<Edit className="text-neutral-300" size={18} />
|
||||||
<Trash2 className="text-red-500 cursor-pointer" size={18} />
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleDelete(item.topic)}
|
||||||
|
className="text-red-500 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
disabled={deleteTopic.isPending}
|
||||||
|
aria-label={`Hapus topic ${item.topic}`}
|
||||||
|
>
|
||||||
|
<Trash2 size={18} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{pending.open && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
||||||
|
<div className="w-full max-w-sm rounded-md bg-white p-4 shadow-lg transition-all duration-200 ease-out">
|
||||||
|
<h3 className="text-base font-semibold">Hapus Topic</h3>
|
||||||
|
<p className="mt-2 text-sm text-neutral-600">
|
||||||
|
Yakin ingin menghapus topic "{pending.topic}"?
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="rounded-md border border-neutral-300 px-4 py-2 text-sm"
|
||||||
|
disabled={deleteTopic.isPending}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
className="rounded-md bg-red-500 px-4 py-2 text-sm text-white disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled={deleteTopic.isPending}
|
||||||
|
>
|
||||||
|
{deleteTopic.isPending ? "Menghapus..." : "Hapus"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,3 +35,28 @@ export const getTopicsCommand = async (): Promise<TopicData> => {
|
|||||||
|
|
||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createTopic = async (
|
||||||
|
payload: TopicPayload,
|
||||||
|
): Promise<TopicMutationResponse> => {
|
||||||
|
const res = await api.post("/topics/v1/", payload, {
|
||||||
|
headers: {
|
||||||
|
Authorization: basicAuth(ENV.basicUsername, ENV.basicPassword),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTopic = async (
|
||||||
|
payload: TopicPayload,
|
||||||
|
): Promise<TopicMutationResponse> => {
|
||||||
|
const res = await api.delete("/topics/v1/", {
|
||||||
|
data: payload,
|
||||||
|
headers: {
|
||||||
|
Authorization: basicAuth(ENV.basicUsername, ENV.basicPassword),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|||||||
11
src/repositories/device/types.d.ts
vendored
11
src/repositories/device/types.d.ts
vendored
@@ -51,3 +51,14 @@ type TopicData = {
|
|||||||
message: string;
|
message: string;
|
||||||
data: { topic: string }[];
|
data: { topic: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TopicPayload = {
|
||||||
|
topic: string;
|
||||||
|
type: "state-reply" | "commands";
|
||||||
|
};
|
||||||
|
|
||||||
|
type TopicMutationResponse = {
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
data?: unknown;
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { generateSignature, parseQueryParamsManual } from "./signer";
|
import { generateSignature, parseQueryParamsManual } from "./signer";
|
||||||
import { ENV } from "../constants/env";
|
import { ENV } from "../constants/env";
|
||||||
|
import { getMerchant } from "./storage";
|
||||||
|
|
||||||
import type { InternalAxiosRequestConfig } from "axios";
|
import type { InternalAxiosRequestConfig } from "axios";
|
||||||
|
|
||||||
@@ -17,6 +18,18 @@ const api = axios.create({
|
|||||||
|
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config: SignedAxiosRequestConfig) => {
|
(config: SignedAxiosRequestConfig) => {
|
||||||
|
const isTopicsRequest = (config.url ?? "").includes("/topics/");
|
||||||
|
if (isTopicsRequest) {
|
||||||
|
const merchant = getMerchant();
|
||||||
|
const credential = merchant?.credential?.trim();
|
||||||
|
const expectedCredential = ENV.credential?.trim();
|
||||||
|
if (!credential || !expectedCredential || credential !== expectedCredential) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("Credential tidak sesuai. Request dibatalkan."),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (config.signed === false) {
|
if (config.signed === false) {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user