feat: add Revolut payment link support alongside IBAN
- Add NoteBox component for displaying warning messages with icon - Add Revolut profile name field to user settings schema - Update UserSettingsForm to support payment instruction selection (disabled/IBAN/Revolut) - Add Croatian and English translations for new payment options - Reserve fields for future per-instruction enable/disable functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -28,8 +28,18 @@ export interface UserSettings {
|
||||
ownerIBAN?: string | null;
|
||||
/** currency (ISO 4217) */
|
||||
currency?: string | null;
|
||||
/** owner Revolut payment link */
|
||||
ownerRevolutProfileName?: string | null;
|
||||
/** whether to show 2D code in monthly statement */
|
||||
show2dCodeInMonthlyStatement?: boolean | null;
|
||||
/** whether to show payment instructions in monthly statement */
|
||||
showPaymentInstructionsInMonthlyStatement?: "disabled" | "iban" | "revolut" | null;
|
||||
|
||||
// /** whether enableshow IBAN payment instructions in monthly statement */
|
||||
// enableIbanPaymentInstructionsInMonthlyStatement?: boolean | null;
|
||||
// /** whether to enable Revolut payment instructions in monthly statement */
|
||||
// enableRevolutPaymentInstructionsInMonthlyStatement?: boolean | null;
|
||||
|
||||
};
|
||||
|
||||
/** bill object in the form returned by MongoDB */
|
||||
|
||||
7
app/ui/NoteBox.tsx
Normal file
7
app/ui/NoteBox.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
export const NoteBox: FC<{ children: ReactNode, className?: string }> = ({ children, className }) =>
|
||||
<div className={`alert max-w-md flex flex-row items-start gap-[.8rem] ml-1 ${className}`}>
|
||||
<span className="w-6 h-6 text-xl">⚠️</span>
|
||||
<span className="text-left">{children}</span>
|
||||
</div>
|
||||
@@ -9,6 +9,7 @@ import Link from "next/link";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import { formatIban } from "../lib/formatStrings";
|
||||
import { InfoBox } from "./InfoBox";
|
||||
import { NoteBox } from "./NoteBox";
|
||||
|
||||
export type UserSettingsFormProps = {
|
||||
userSettings: UserSettings | null;
|
||||
@@ -32,21 +33,16 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
|
||||
ownerTown: userSettings?.ownerTown ?? "",
|
||||
ownerIBAN: formatIban(userSettings?.ownerIBAN) ?? "",
|
||||
currency: userSettings?.currency ?? "EUR",
|
||||
ownerRevolutProfileName: userSettings?.ownerRevolutProfileName ?? "",
|
||||
showPaymentInstructions: userSettings?.showPaymentInstructionsInMonthlyStatement ?? "disabled",
|
||||
});
|
||||
|
||||
// https://revolut.me/aderezic?currency=EUR&amount=70000
|
||||
|
||||
const handleInputChange = (field: keyof typeof formValues, value: string) => {
|
||||
setFormValues(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// Check if any required field is missing (clean IBAN of spaces for validation)
|
||||
const cleanedOwnerIBAN = formValues.ownerIBAN.replace(/\s/g, '');
|
||||
const hasMissingData = !formValues.ownerName || !formValues.ownerStreet || !formValues.ownerTown || !cleanedOwnerIBAN || !formValues.currency;
|
||||
|
||||
// Track whether to generate 2D code for tenant (use persisted value from database)
|
||||
const [show2dCodeInMonthlyStatement, setShow2dCodeInMonthlyStatement] = useState(
|
||||
userSettings?.show2dCodeInMonthlyStatement ?? false
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<fieldset className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4 pt-1 pb-2 mt-4">
|
||||
@@ -60,7 +56,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
|
||||
id="currency"
|
||||
name="currency"
|
||||
className="select select-bordered w-full"
|
||||
defaultValue={userSettings?.currency ?? "EUR"}
|
||||
defaultValue={formValues.currency}
|
||||
onChange={(e) => handleInputChange("currency", e.target.value)}
|
||||
disabled={pending}
|
||||
>
|
||||
@@ -111,25 +107,28 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4 pb-2 mt-4">
|
||||
<legend className="fieldset-legend font-semibold uppercase">{t("tenant-2d-code-legend")}</legend>
|
||||
<legend className="fieldset-legend font-semibold uppercase">{t("tenant-payment-instructions--legend")}</legend>
|
||||
|
||||
<InfoBox className="p-1 mb-1">{t("info-box-message")}</InfoBox>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<label className="label cursor-pointer justify-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="generateTenantCode"
|
||||
className="toggle toggle-primary"
|
||||
checked={show2dCodeInMonthlyStatement}
|
||||
onChange={(e) => setShow2dCodeInMonthlyStatement(e.target.checked)}
|
||||
/>
|
||||
<legend className="fieldset-legend">{t("tenant-2d-code-toggle-label")}</legend>
|
||||
</label>
|
||||
<fieldset className="form-control w-full">
|
||||
<select
|
||||
id="showPaymentInstructions"
|
||||
name="showPaymentInstructions"
|
||||
className="select select-bordered w-full"
|
||||
defaultValue={formValues.showPaymentInstructions}
|
||||
onChange={(e) => handleInputChange("showPaymentInstructions", e.target.value)}
|
||||
disabled={pending}
|
||||
>
|
||||
<option value="disabled">{t("tenant-payment-instructions--show-no-instructions")}</option>
|
||||
<option value="iban">{t("tenant-payment-instructions--show-iban-instructions")}</option>
|
||||
<option value="revolut">{t("tenant-payment-instructions--show-revolut-instructions")}</option>
|
||||
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
{show2dCodeInMonthlyStatement && (
|
||||
{formValues.showPaymentInstructions === "iban" && (
|
||||
<>
|
||||
<div className="divider mt-6 mb-2 font-bold uppercase">Informacije za uplatu</div>
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text">{t("owner-name-label")}</span>
|
||||
@@ -141,7 +140,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
|
||||
maxLength={25}
|
||||
placeholder={t("owner-name-placeholder")}
|
||||
className="input input-bordered w-full placeholder:text-gray-600"
|
||||
defaultValue={userSettings?.ownerName ?? ""}
|
||||
defaultValue={formValues.ownerName}
|
||||
onChange={(e) => handleInputChange("ownerName", e.target.value)}
|
||||
disabled={pending}
|
||||
/>
|
||||
@@ -166,7 +165,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
|
||||
maxLength={25}
|
||||
placeholder={t("owner-street-placeholder")}
|
||||
className="input input-bordered w-full placeholder:text-gray-600"
|
||||
defaultValue={userSettings?.ownerStreet ?? ""}
|
||||
defaultValue={formValues.ownerStreet}
|
||||
onChange={(e) => handleInputChange("ownerStreet", e.target.value)}
|
||||
disabled={pending}
|
||||
/>
|
||||
@@ -191,7 +190,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
|
||||
maxLength={27}
|
||||
placeholder={t("owner-town-placeholder")}
|
||||
className="input input-bordered w-full placeholder:text-gray-600"
|
||||
defaultValue={userSettings?.ownerTown ?? ""}
|
||||
defaultValue={formValues.ownerTown}
|
||||
onChange={(e) => handleInputChange("ownerTown", e.target.value)}
|
||||
disabled={pending}
|
||||
/>
|
||||
@@ -215,7 +214,7 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
|
||||
type="text"
|
||||
placeholder={t("owner-iban-placeholder")}
|
||||
className="input input-bordered w-full placeholder:text-gray-600"
|
||||
defaultValue={formatIban(userSettings?.ownerIBAN)}
|
||||
defaultValue={formValues.ownerIBAN}
|
||||
onChange={(e) => handleInputChange("ownerIBAN", e.target.value)}
|
||||
disabled={pending}
|
||||
/>
|
||||
@@ -229,9 +228,39 @@ const FormFields: FC<FormFieldsProps> = ({ userSettings, errors, message }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InfoBox className="p-1 mt-1">{t("additional-notes")}</InfoBox>
|
||||
<NoteBox className="p-1 mt-1">{t("payment-additional-notes")}</NoteBox>
|
||||
</>
|
||||
)}
|
||||
{formValues.showPaymentInstructions === "revolut" && (
|
||||
<>
|
||||
<div className="divider mt-6 mb-2 font-bold uppercase">Informacije za uplatu</div>
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text">{t("owner-revolut-link-label")}</span>
|
||||
</label>
|
||||
<input
|
||||
id="ownerRevolutProfileName"
|
||||
name="ownerRevolutProfileName"
|
||||
type="text"
|
||||
maxLength={25}
|
||||
placeholder={t("owner-revolut-link-placeholder")}
|
||||
className="input input-bordered w-full placeholder:text-gray-600"
|
||||
defaultValue={formValues.ownerRevolutProfileName}
|
||||
onChange={(e) => handleInputChange("ownerRevolutProfileName", e.target.value)}
|
||||
disabled={pending}
|
||||
/>
|
||||
<div id="ownerRevolutProfileName-error" aria-live="polite" aria-atomic="true">
|
||||
{errors?.ownerRevolutProfileName &&
|
||||
errors.ownerRevolutProfileName.map((error: string) => (
|
||||
<p className="mt-2 text-sm text-red-500" key={error}>
|
||||
{error}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<NoteBox className="p-1 mt-1">{t("payment-additional-notes")}</NoteBox>
|
||||
</>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<div id="general-error" aria-live="polite" aria-atomic="true">
|
||||
|
||||
Reference in New Issue
Block a user