Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,6 +1,6 @@
"use client";

import { FC } from "react";
import { type FC } from "react";
import { ExternalLinkOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/external_link_outlined";
import { Typography } from "@oko-wallet/oko-common-ui/typography";
import { Spacing } from "@oko-wallet/oko-common-ui/spacing";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { FC, useState } from "react";
import { type FC, useState } from "react";
import { Badge } from "@oko-wallet/oko-common-ui/badge";
import { CopyOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/copy_outlined";
import { EyeIcon } from "@oko-wallet/oko-common-ui/icons/eye";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"use client";

import { PropsWithChildren, useEffect, useState } from "react";
import { type PropsWithChildren, useEffect, useState, type FC } from "react";
import { useRouter } from "next/navigation";

import { paths } from "@oko-wallet-ct-dashboard/paths";
import { useAppState } from "@oko-wallet-ct-dashboard/state";

export const Authorized: React.FC<PropsWithChildren> = ({ children }) => {
export const Authorized: FC<PropsWithChildren> = ({ children }) => {
const router = useRouter();
const user = useAppState((state) => state.user);
const token = useAppState((state) => state.token);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PropsWithChildren, FC } from "react";
import type { PropsWithChildren, FC } from "react";
import cn from "classnames";

import styles from "./dashboard_body.module.scss";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,61 @@
width: 100%;
margin-top: 8px;
}

.themeSection {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}

.themeHeader {
display: flex;
flex-direction: column;
gap: 4px;
}

.themeLabel {
color: var(--text-secondary);
font-size: 14px;
font-weight: 600;
line-height: 20px;
}

.themeDescription {
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
line-height: 18px;
}

.themeOptions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}

.themeOptionButton {
padding: 8px 12px;
border-radius: 10px;
border: 1px solid var(--border-primary);
background: var(--bg-primary);
color: var(--text-primary);

cursor: pointer;
transition: all 0.2s ease;

&:hover {
border-color: #d0d5dd;
}

&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
}

.themeOptionButtonActive {
background: var(--bg-brand-solid);
color: var(--text-primary-on-brand);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
"use client";

import React, { useRef, useState } from "react";
import {
type ChangeEvent,
useRef,
useState,
type DragEvent,
type FC,
} from "react";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { Input } from "@oko-wallet/oko-common-ui/input";
import { Button } from "@oko-wallet/oko-common-ui/button";
import { PlusIcon } from "@oko-wallet/oko-common-ui/icons/plus";
import { XCloseIcon } from "@oko-wallet/oko-common-ui/icons/x_close";
import type { CustomerTheme } from "@oko-wallet/oko-types/customers";
import { Typography } from "@oko-wallet/oko-common-ui/typography";

import { useCustomerInfo } from "@oko-wallet-ct-dashboard/hooks/use_customer_info";
import { useAppState } from "@oko-wallet-ct-dashboard/state";
import { requestUpdateCustomerInfo } from "@oko-wallet-ct-dashboard/fetch/customers";
import styles from "./edit_info_form.module.scss";

export const EditInfoForm = () => {
const THEME_OPTIONS: CustomerTheme[] = ["light", "dark", "system"];

export const EditInfoForm: FC = () => {
const router = useRouter();
const queryClient = useQueryClient();
const customer = useCustomerInfo();
Expand All @@ -23,6 +33,10 @@ export const EditInfoForm = () => {

const [label, setLabel] = useState(customer.data?.label ?? "");
const [url, setUrl] = useState(customer.data?.url ?? "");
const [theme, setTheme] = useState<CustomerTheme>(
customer.data?.theme ?? "system",
);

const [logoFile, setLogoFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(
customer.data?.logo_url ?? null,
Expand Down Expand Up @@ -95,7 +109,7 @@ export const EditInfoForm = () => {
}
};

const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
handleLogoUpload(file);
Expand All @@ -107,29 +121,29 @@ export const EditInfoForm = () => {
};

// Drag and drop handlers
const handleDragEnter = (e: React.DragEvent) => {
const handleDragEnter = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};

const handleDragLeave = (e: React.DragEvent) => {
const handleDragLeave = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};

const handleDragOver = (e: React.DragEvent) => {
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
};

const handleDrop = (e: React.DragEvent) => {
const handleDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);

const file = e.dataTransfer.files?.[0];
const file = e.dataTransfer?.files?.[0];
if (file) {
handleLogoUpload(file);
}
Expand All @@ -147,12 +161,23 @@ export const EditInfoForm = () => {
const hasLabelChange = label !== customer.data?.label;
const hasUrlChange = url !== (customer.data?.url ?? "");
const hasLogoChange = logoFile !== null || shouldDeleteLogo;
const hasThemeChange = theme !== customer.data?.theme;

if (!hasLabelChange && !hasUrlChange && !hasLogoChange) {
if (!hasLabelChange && !hasUrlChange && !hasLogoChange && !hasThemeChange) {
setError("No changes to save.");
return;
}

if (!url || url.trim() === "") {
setError("App URL is required.");
return;
}

if (hasUrlChange && !validateUrl(url)) {
setError("App URL format is invalid.");
return;
}

setIsLoading(true);
setError(null);

Expand All @@ -162,6 +187,7 @@ export const EditInfoForm = () => {
label: hasLabelChange ? label : undefined,
url: hasUrlChange ? url : undefined,
logoFile: logoFile,
theme: hasThemeChange ? theme : undefined,
deleteLogo: shouldDeleteLogo,
});

Expand Down Expand Up @@ -189,7 +215,8 @@ export const EditInfoForm = () => {
label !== customer.data?.label ||
url !== (customer.data?.url ?? "") ||
logoFile !== null ||
shouldDeleteLogo;
shouldDeleteLogo ||
theme !== customer.data?.theme;

return (
<form onSubmit={handleSubmit} className={styles.form}>
Expand All @@ -213,6 +240,46 @@ export const EditInfoForm = () => {
className={styles.input}
/>

<div className={styles.themeSection}>
<div className={styles.themeHeader}>
<span className={styles.themeLabel}>Oko Wallet Theme</span>
<span className={styles.themeDescription}>
Choose the default theme for the Oko wallet.
</span>
</div>

<div className={styles.themeOptions}>
{THEME_OPTIONS.map((option) => {
const label =
option === "system"
? "System"
: option === "light"
? "Light"
: "Dark";

return (
<button
key={option}
type="button"
className={`${styles.themeOptionButton} ${
theme === option ? styles.themeOptionButtonActive : ""
}`}
onClick={() => setTheme(option)}
disabled={isLoading}
>
<Typography
size="sm"
weight="medium"
color={theme === option ? "primary-on-brand" : "primary"}
>
{label}
</Typography>
</button>
);
})}
</div>
</div>

{/* Logo Upload with drag & drop */}
<div className={styles.appLogoUploadWrapper}>
<label className={styles.appLogoUploadLabel}>
Expand Down Expand Up @@ -289,3 +356,12 @@ export const EditInfoForm = () => {
</form>
);
};

function validateUrl(url: string): boolean {
try {
const urlObj = new URL(url);
return urlObj.protocol === "https:" || urlObj.protocol === "http:";
} catch {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"use client";

import { useEffect, useState, ReactNode, FC, useCallback } from "react";
import {
useEffect,
useState,
type ReactNode,
type FC,
useCallback,
} from "react";

type ExpiryTimerProps = {
duration?: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC } from "react";
import { type FC } from "react";
import Link from "next/link";
import { ExternalLinkOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/external_link_outlined";
import { Typography } from "@oko-wallet/oko-common-ui/typography";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC } from "react";
import { type FC } from "react";
import Link from "next/link";
import { ArrowRightOutlinedIcon } from "@oko-wallet/oko-common-ui/icons/arrow_right_outlined";
import { Typography } from "@oko-wallet/oko-common-ui/typography";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import React, { PropsWithChildren } from "react";
import { type FC, type PropsWithChildren } from "react";

function makeTanStackQueryClient() {
// Create a client
Expand All @@ -16,7 +16,7 @@ function makeTanStackQueryClient() {

const queryClient = makeTanStackQueryClient();

export const Providers: React.FC<PropsWithChildren> = ({ children }) => {
export const Providers: FC<PropsWithChildren> = ({ children }) => {
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={dehydrate(queryClient)}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react";
import { useForm, SubmitHandler } from "react-hook-form";
import { useForm, type SubmitHandler } from "react-hook-form";
import { useRouter } from "next/navigation";

import { paths } from "@oko-wallet-ct-dashboard/paths";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useForm, SubmitHandler, Resolver } from "react-hook-form";
import { useForm, type SubmitHandler, type Resolver } from "react-hook-form";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";

Expand Down
7 changes: 7 additions & 0 deletions apps/customer_dashboard/src/fetch/customers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { APIKey } from "@oko-wallet/oko-types/ct_dashboard";
import type {
Customer,
CustomerTheme,
UpdateCustomerInfoResponse,
} from "@oko-wallet/oko-types/customers";

Expand Down Expand Up @@ -55,12 +56,14 @@ export async function requestUpdateCustomerInfo({
url,
logoFile,
deleteLogo,
theme,
}: {
token: string;
label?: string;
url?: string;
logoFile?: File | null;
deleteLogo?: boolean;
theme?: CustomerTheme;
}) {
// Note: Reasons for using FormData:
// - Need to send file upload (logo) and text data together
Expand All @@ -83,6 +86,10 @@ export async function requestUpdateCustomerInfo({
formData.append("delete_logo", "true");
}

if (theme !== undefined) {
formData.append("theme", theme);
}

return errorHandle<UpdateCustomerInfoResponse>(() =>
fetch(`${CUSTOMER_V1_ENDPOINT}/customer/update_info`, {
method: "POST",
Expand Down
11 changes: 0 additions & 11 deletions apps/customer_dashboard/src/fetch/types.ts

This file was deleted.

4 changes: 2 additions & 2 deletions apps/customer_dashboard/src/fetch/users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { OkoApiResponse } from "@oko-wallet/oko-types/api_response";
import {
import type { OkoApiResponse } from "@oko-wallet/oko-types/api_response";
import type {
ChangePasswordResponse,
LoginResponse,
SendVerificationResponse,
Expand Down
Loading