Skip to content

Commit 99b9065

Browse files
authored
Merge pull request #414 from SweetBoy-eth/fix/issues-379-382
2 parents 47b1cfe + 7ca46d3 commit 99b9065

9 files changed

Lines changed: 560 additions & 17 deletions

File tree

app/(merchant)/payments/page.tsx

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
2323
import { trimInput } from '@/lib/utils/sanitize';
2424
import { useNotify } from '@/lib/hooks/useNotify';
2525
import { usePayments, type ApiPayment } from '@/lib/api/hooks';
26+
import { apiClient } from '@/lib/api/axios';
2627
import Link from 'next/link';
2728

2829
type PaymentLink = ApiPayment;
@@ -99,7 +100,7 @@ const PaymentLinkCard = memo(function PaymentLinkCard({ link, onEdit, onDelete,
99100

100101
export default function PaymentsPage() {
101102
const { data: links, isLoading, error: fetchError, refetch } = usePayments();
102-
const { success: notifySuccess, info: notifyInfo } = useNotify();
103+
const { success: notifySuccess, info: notifyInfo, error: notifyError } = useNotify();
103104

104105
// Filter & Search states
105106
const [searchTerm, setSearchTerm] = useState('');
@@ -116,6 +117,7 @@ export default function PaymentsPage() {
116117
const [deletingLink, setDeletingLink] = useState<PaymentLink | null>(null);
117118
const [selectedQrLink, setSelectedQrLink] = useState<PaymentLink | null>(null);
118119
const [linksError, setLinksError] = useState(false);
120+
const [isCreating, setIsCreating] = useState(false);
119121

120122
// Form states
121123
const [labelValue, setLabelValue] = useState('');
@@ -156,17 +158,50 @@ export default function PaymentsPage() {
156158
return filteredLinks.slice(start, start + pageSize);
157159
}, [filteredLinks, currentPage, pageSize]);
158160

159-
const handleCreate = (e: React.FormEvent) => {
161+
const handleCreate = async (e: React.FormEvent) => {
160162
e.preventDefault();
161163
const sanitizedLabel = trimInput(labelValue);
162164
if (!sanitizedLabel) {
163165
setLabelError('Label is required');
164166
return;
165167
}
166168
setLabelError('');
167-
notifySuccess('Payment link created successfully');
168-
setIsCreateOpen(false);
169-
resetForm();
169+
170+
const payload: Record<string, unknown> = {
171+
label: sanitizedLabel,
172+
currency: currencyValue,
173+
mode: currencyMode,
174+
reference: referenceValue || undefined,
175+
expiry: expiryValue || undefined,
176+
redirectUrl: redirectUrlValue || undefined,
177+
};
178+
179+
if (currencyMode === 'single') {
180+
payload.amount = amountValue ? parseFloat(amountValue) : undefined;
181+
} else {
182+
const amounts: Record<string, number> = {};
183+
for (const [code, val] of Object.entries(multiCurrencyAmounts)) {
184+
if (val) amounts[code] = parseFloat(val);
185+
}
186+
payload.amounts = Object.keys(amounts).length > 0 ? amounts : undefined;
187+
payload.currencies = selectedCurrencies;
188+
}
189+
190+
setIsCreating(true);
191+
try {
192+
await apiClient.post('/api/payment-links', payload);
193+
notifySuccess('Payment link created successfully');
194+
setIsCreateOpen(false);
195+
resetForm();
196+
refetch();
197+
} catch (err: unknown) {
198+
const message =
199+
(err as { response?: { data?: { error?: string } } })?.response?.data?.error ??
200+
'Failed to create payment link';
201+
notifyError(message);
202+
} finally {
203+
setIsCreating(false);
204+
}
170205
};
171206

172207
const { register: registerEdit, handleSubmit: handleEditSubmitForm, reset: resetEditForm, formState: { errors: editErrors } } = useForm<EditPaymentLinkFormValues>({
@@ -375,7 +410,9 @@ export default function PaymentsPage() {
375410

376411
<DialogFooter className="pt-4">
377412
<Button type="button" variant="ghost" onClick={() => setIsCreateOpen(false)}>Cancel</Button>
378-
<Button type="submit">Create Link</Button>
413+
<Button type="submit" disabled={isCreating}>
414+
{isCreating ? 'Creating...' : 'Create Link'}
415+
</Button>
379416
</DialogFooter>
380417
</form>
381418
</DialogContent>

app/(merchant)/settings/page.tsx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,64 @@ export default function SettingsPage() {
7070
const [currentPassword, setCurrentPassword] = useState('');
7171
const [newPassword, setNewPassword] = useState('');
7272
const [confirmNewPassword, setConfirmNewPassword] = useState('');
73+
const [passwordErrors, setPasswordErrors] = useState<{
74+
current?: string;
75+
newPass?: string;
76+
confirm?: string;
77+
}>({});
78+
79+
const validatePassword = useCallback(() => {
80+
const errors: typeof passwordErrors = {};
81+
82+
if (!currentPassword) {
83+
errors.current = 'Current password is required';
84+
}
85+
86+
if (newPassword.length < 8) {
87+
errors.newPass = 'Password must be at least 8 characters';
88+
} else if (!/[0-9]/.test(newPassword)) {
89+
errors.newPass = 'Password must contain at least one number';
90+
} else if (!/[A-Z]/.test(newPassword)) {
91+
errors.newPass = 'Password must contain at least one uppercase letter';
92+
}
93+
94+
if (confirmNewPassword !== newPassword) {
95+
errors.confirm = 'Passwords do not match';
96+
}
97+
98+
setPasswordErrors(errors);
99+
return Object.keys(errors).length === 0;
100+
}, [currentPassword, newPassword, confirmNewPassword]);
101+
102+
const isPasswordValid = useMemo(() => {
103+
return (
104+
currentPassword.length > 0 &&
105+
newPassword.length >= 8 &&
106+
/[0-9]/.test(newPassword) &&
107+
/[A-Z]/.test(newPassword) &&
108+
confirmNewPassword === newPassword
109+
);
110+
}, [currentPassword, newPassword, confirmNewPassword]);
111+
112+
const handlePasswordChange = useCallback(async () => {
113+
if (!validatePassword()) return;
114+
setIsSubmitting(true);
115+
try {
116+
await apiClient.post('/api/auth/change-password', {
117+
currentPassword,
118+
newPassword,
119+
});
120+
notify.success('Password updated');
121+
setCurrentPassword('');
122+
setNewPassword('');
123+
setConfirmNewPassword('');
124+
setPasswordErrors({});
125+
} catch {
126+
notify.error('Failed to update password');
127+
} finally {
128+
setIsSubmitting(false);
129+
}
130+
}, [currentPassword, newPassword, validatePassword, notify]);
73131

74132
const [notificationPreferences, setNotificationPreferences] = useState<Record<string, boolean>>({
75133
paymentReceived: true,
@@ -349,18 +407,22 @@ export default function SettingsPage() {
349407
<div className="space-y-1.5">
350408
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Current Password</Label>
351409
<Input value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} type="password" placeholder="••••••••" className="h-10 border-border rounded-xl bg-card text-sm" />
410+
{passwordErrors.current && <p className="text-xs text-destructive mt-1">{passwordErrors.current}</p>}
352411
</div>
353412
<div className="space-y-1.5">
354413
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">New Password</Label>
355414
<Input value={newPassword} onChange={(e) => setNewPassword(e.target.value)} type="password" placeholder="••••••••" className="h-10 border-border rounded-xl bg-card text-sm" />
415+
{passwordErrors.newPass && <p className="text-xs text-destructive mt-1">{passwordErrors.newPass}</p>}
356416
</div>
357417
<div className="space-y-1.5">
358418
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Confirm New Password</Label>
359419
<Input value={confirmNewPassword} onChange={(e) => setConfirmNewPassword(e.target.value)} type="password" placeholder="••••••••" className="h-10 border-border rounded-xl bg-card text-sm" />
420+
{passwordErrors.confirm && <p className="text-xs text-destructive mt-1">{passwordErrors.confirm}</p>}
360421
</div>
361422
<Button
362423
className="bg-primary hover:bg-primary/90 text-primary-foreground font-semibold rounded-xl h-10 px-6 text-sm"
363-
onClick={() => notify.success('Password updated')}
424+
onClick={handlePasswordChange}
425+
disabled={!isPasswordValid || isSubmitting}
364426
>
365427
Update Password
366428
</Button>

components/i18n/I18nProvider.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,31 @@
22

33
import { ReactNode, useEffect, useState } from "react";
44
import { createInstance } from "i18next";
5-
import { I18nextProvider } from "react-i18next";
5+
import { initReactI18next } from "react-i18next";
6+
import HttpBackend from "i18next-http-backend";
67

7-
import { defaultLocale, detectPreferredLocale, resources } from "@/lib/i18n/config";
8+
import { defaultLocale, detectPreferredLocale, fallbackResources } from "@/lib/i18n/config";
89

910
export function I18nProvider({ children }: { children: ReactNode }) {
1011
const [i18n] = useState(() => {
1112
const instance = createInstance();
12-
void instance.init({
13-
resources,
14-
lng: defaultLocale,
15-
fallbackLng: defaultLocale,
16-
interpolation: { escapeValue: false },
17-
initAsync: false,
18-
});
13+
void instance
14+
.use(HttpBackend)
15+
.use(initReactI18next)
16+
.init({
17+
fallbackLng: defaultLocale,
18+
supportedLngs: ["en", "fr", "pt", "sw"],
19+
ns: ["translation"],
20+
defaultNS: "translation",
21+
backend: {
22+
loadPath: "/locales/{{lng}}/{{ns}}.json",
23+
crossOrigin: true,
24+
},
25+
resources: fallbackResources,
26+
interpolation: { escapeValue: false },
27+
initAsync: false,
28+
react: { useSuspense: false },
29+
});
1930
return instance;
2031
});
2132

0 commit comments

Comments
 (0)