Browse Source

feat: add new UI assets and update webview actions and question sheet component

master
mortezaei 1 week ago
parent
commit
30e20c2efa
  1. 101
      src/components/Componentes/question-sheet.test.tsx
  2. 10
      src/components/Componentes/question-sheet.tsx
  3. 18
      src/lib/webview-actions.ts

101
src/components/Componentes/question-sheet.test.tsx

@ -214,14 +214,14 @@ describe("QuestionSheet component", () => {
pushState.mockRestore();
});
it("opens a searchable sheet without focusing search", () => {
it("opens a searchable sheet for 7+ options without focusing search", () => {
const question = {
id: "q_country",
title: "کشور",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب کشور" },
options: Array.from({ length: 6 }, (_, index) => ({
options: Array.from({ length: 7 }, (_, index) => ({
id: `country-${index}`,
value: `country-${index}`,
label: `کشور ${index + 1}`,
@ -246,7 +246,7 @@ describe("QuestionSheet component", () => {
);
});
it("sizes a short options sheet to its content", () => {
it("sizes a short options sheet to its content (4 options)", () => {
const question = {
id: "q_short",
title: "انتخاب کوتاه",
@ -277,4 +277,99 @@ describe("QuestionSheet component", () => {
"max-h-[82svh]",
);
});
it("compact boundary: 5 options → compact, no search", () => {
const question = {
id: "q_boundary_5",
title: "پنج گزینه",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب" },
options: Array.from({ length: 5 }, (_, i) => ({
id: `opt-${i}`,
value: `opt-${i}`,
label: `گزینه ${i + 1}`,
order: i + 1,
})),
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "انتخاب" }));
const section = screen.getByRole("dialog").querySelector("section");
expect(section).toHaveClass("h-auto", "max-h-[82svh]");
expect(screen.queryByPlaceholderText("جستجو...")).toBeNull();
});
it("compact boundary: 6 options → compact, no search", () => {
const question = {
id: "q_boundary_6",
title: "شش گزینه",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب" },
options: Array.from({ length: 6 }, (_, i) => ({
id: `opt-${i}`,
value: `opt-${i}`,
label: `گزینه ${i + 1}`,
order: i + 1,
})),
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "انتخاب" }));
const section = screen.getByRole("dialog").querySelector("section");
expect(section).toHaveClass("h-auto", "max-h-[82svh]");
expect(section).not.toHaveClass("h-[82svh]");
expect(screen.queryByPlaceholderText("جستجو...")).toBeNull();
});
it("compact boundary: 7 options → large sheet with search", () => {
const question = {
id: "q_boundary_7",
title: "هفت گزینه",
type: "dropdown",
required: true,
extras: { placeHolder: "انتخاب" },
options: Array.from({ length: 7 }, (_, i) => ({
id: `opt-${i}`,
value: `opt-${i}`,
label: `گزینه ${i + 1}`,
order: i + 1,
})),
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[question]}>
<QuestionSheet question={question} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
fireEvent.click(screen.getByRole("button", { name: "انتخاب" }));
const section = screen.getByRole("dialog").querySelector("section");
expect(section).toHaveClass("h-[82svh]");
expect(section).not.toHaveClass("h-auto");
expect(screen.getByPlaceholderText("جستجو...")).toBeDefined();
});
});

10
src/components/Componentes/question-sheet.tsx

@ -73,11 +73,11 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
const options = question.options || [];
const showSearch = (() => {
if (question.extras?.noSearch) return false;
return options.length > 5;
})();
const isCompact = options.length <= 5;
const COMPACT_OPTIONS_MAX = 6;
const isCompact = options.length <= COMPACT_OPTIONS_MAX;
const showSearch =
!question.extras?.noSearch && options.length > COMPACT_OPTIONS_MAX;
useEffect(() => {
if (!isOpen || isClosing || !isCompact) return;

18
src/lib/webview-actions.ts

@ -2,7 +2,7 @@
* WebView Utility Actions
*
* Helper module for sending utility actions to Flutter via HabibApp channel.
* Actions: download_file, copy_to_clipboard, open_external_url, upload_file
* Actions: download_file, copy_to_clipboard, open_external_url, upload_file, buy_coin
*
* Docs: WEBVIEW_ACTION_DOWNLOAD.md
*/
@ -150,3 +150,19 @@ export function uploadFile(options: UploadFileOptions): boolean {
options as unknown as Record<string, unknown>,
);
}
// ─── buy_coin ────────────────────────────────────────────
/**
* Ask Flutter to open the Habib Coin packages list page (Add Coin).
*
* Used when a payment fails with "Not enough coins": the user tops up
* natively, returns to the WebView and retries the payment.
*
* ```ts
* buyHabibCoinPackages();
* ```
*/
export function buyHabibCoinPackages(): boolean {
return postActionToFlutter("buy_coin");
}
Loading…
Cancel
Save