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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
!.github/workflows/build.yml
app/app
web/web
/venv
5 changes: 5 additions & 0 deletions app/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
urnetwork-webmanager

## Development

Create an `.env` file and add `VITE_API_BASE=/api`
Run `npm run dev` to start the development server.
107 changes: 104 additions & 3 deletions app/src/components/AccountSettingsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useState, useEffect } from 'react';
import { Settings, Key, Copy, Clock, Users, AlertCircle, CheckCircle, Shield, Lock, CreditCard, ExternalLink } from 'lucide-react';
import React, { useState, useEffect, useCallback } from 'react';
import { Settings, Key, Copy, Clock, Users, AlertCircle, CheckCircle, Shield, Lock, CreditCard, ExternalLink, TicketSlash, TicketCheck } from 'lucide-react';
import { useAuth } from '../hooks/useAuth';
import { createAuthCode, fetchNetworkUser } from '../services/api';
import { createAuthCode, fetchNetworkTransferBalanceCodes, fetchNetworkUser } from '../services/api';
import type { CreateAuthCodeResponse } from '../services/api';
import toast from 'react-hot-toast';
import PasswordResetModal from './PasswordResetModal';
import { RedeemedTransferBalanceCode } from '../services/types';
import RedeemTransferBalanceCodeModal from './RedeemTransferBalanceCodeModal';

const AccountSettingsSection: React.FC = () => {
const { token } = useAuth();
Expand All @@ -14,9 +16,32 @@
const [authCodeResponse, setAuthCodeResponse] = useState<CreateAuthCodeResponse | null>(null);
const [copiedToClipboard, setCopiedToClipboard] = useState(false);
const [isPasswordResetModalOpen, setIsPasswordResetModalOpen] = useState(false);
const [transferBalanceCodes, setTransferBalanceCodes] = useState<RedeemedTransferBalanceCode[]>([]);
const [isAddTransferBalanceCodeModalOpen, setIsAddTransferBalanceCodeModalOpen] = useState(false);
const [isLoadingTransferBalanceCodes, setIsLoadingTransferBalanceCodes] = useState(true);
const [userEmail, setUserEmail] = useState<string>('');
const [isLoadingUserEmail, setIsLoadingUserEmail] = useState(true);

const loadTransferBalanceCodes = useCallback(async () => {
if (!token) {
setIsLoadingTransferBalanceCodes(false);
return;
}

setIsLoadingTransferBalanceCodes(true);

try {
const response = await fetchNetworkTransferBalanceCodes(token);
if (response.balance_codes) {
setTransferBalanceCodes(response.balance_codes);
}
} catch (error) {
console.error('Failed to fetch network transfer balance codes:', error);
} finally {
setIsLoadingTransferBalanceCodes(false);
}
}, [token]);

useEffect(() => {
const loadUserEmail = async () => {
if (!token) {
Expand All @@ -37,7 +62,8 @@
};

loadUserEmail();
loadTransferBalanceCodes();
}, [token]);

Check warning on line 66 in app/src/components/AccountSettingsSection.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has a missing dependency: 'loadTransferBalanceCodes'. Either include it or remove the dependency array

Check warning on line 66 in app/src/components/AccountSettingsSection.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has a missing dependency: 'loadTransferBalanceCodes'. Either include it or remove the dependency array

const handleGenerateAuthCode = async () => {
if (!token) return;
Expand Down Expand Up @@ -101,6 +127,16 @@
}
};

const maskSecret = (secret: string) => {
if (!secret || secret.length <= 6) return secret;
return `${secret.slice(0, 3)}...${secret.slice(-3)}`;
};

const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleString();
};

return (
<div className="space-y-8">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 animate-staggerFadeUp" style={{ animationDelay: '0.05s' }}>
Expand Down Expand Up @@ -328,6 +364,62 @@
</div>
</div>

{/* Account transfer balance codes */}
<div className="bg-gray-800 rounded-xl shadow-2xl overflow-hidden border border-gray-700 animate-staggerFadeUp" style={{ animationDelay: '0.1s' }}>
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-4 border-b border-gray-600">
<div className="flex items-center gap-3">
<TicketCheck size={20} className="text-white" />
<div>
<h3 className="font-medium text-white">Account Transfer Balance Codes</h3>
<p className="text-blue-100 text-sm mt-1">Redeem a transfer balance code to add data to your account.</p>
</div>
</div>
</div>

<div className="p-6 space-y-6">

<table className="min-w-full divide-y divide-gray-700">
<thead className="bg-gray-900">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Secret</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Redeemed</th>
</tr>
</thead>
<tbody className="bg-gray-800 divide-y divide-gray-700">
{transferBalanceCodes.map((code) => (
<tr key={code.balance_code_id}>
<td className="px-6 py-4 whitespace-nowrap">{maskSecret(code.secret)}</td>
<td className="px-6 py-4 whitespace-nowrap">{formatDate(code.redeem_time)}</td>
</tr>
))}
</tbody>
</table>

{transferBalanceCodes.length === 0 && !isLoadingTransferBalanceCodes && (
<div className="text-center py-12">
<div className="w-16 h-16 bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-4">
<TicketSlash className="text-gray-500" size={24} />
</div>
<h3 className="text-lg font-medium text-gray-200 mb-2">No Transfer Balance Codes Redeemed</h3>
<p className="text-gray-400 italic">No transfer balances codes found for your network.</p>
</div>
)}

<button
onClick={() => setIsAddTransferBalanceCodeModalOpen(true)}
disabled={isGenerating}
className={`flex items-center justify-center gap-2 px-6 py-4 rounded-lg font-medium transition-all duration-200 ${
isGenerating
? 'bg-gray-600 cursor-not-allowed border border-gray-600 text-gray-400'
: 'bg-blue-600 hover:bg-blue-700 text-white border border-blue-500 hover:shadow-lg transform hover:scale-[1.02]'
}`}
>
<TicketCheck size={20} />
<span>Redeem Transfer Balance Code</span>
</button>
</div>
</div>

{/* Password Reset */}
<div className="bg-gray-800 rounded-xl shadow-2xl overflow-hidden border border-gray-700 animate-staggerFadeUp" style={{ animationDelay: '0.15s' }}>
<div className="bg-gradient-to-r from-blue-600 to-cyan-600 px-6 py-4 border-b border-gray-600">
Expand Down Expand Up @@ -408,6 +500,15 @@
onClose={() => setIsPasswordResetModalOpen(false)}
userEmail={userEmail}
/>

<RedeemTransferBalanceCodeModal
isOpen={isAddTransferBalanceCodeModalOpen}
onClose={() => setIsAddTransferBalanceCodeModalOpen(false)}
onSuccess={() => {
// reload balance codes
loadTransferBalanceCodes();
}}
/>
</div>
);
};
Expand Down
231 changes: 231 additions & 0 deletions app/src/components/RedeemTransferBalanceCodeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import React, { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { X, CheckCircle, TicketCheck } from "lucide-react";
import { redeemTransferBalanceCode } from "../services/api";
import toast from "react-hot-toast";
import { useAuth } from "../hooks/useAuth";

interface PasswordResetModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}

type ModalState = "initial" | "loading" | "success";

const RedeemTransferBalanceCodeModal: React.FC<PasswordResetModalProps> = ({
isOpen,
onClose,
onSuccess,
}) => {
const { token } = useAuth();
const modalRef = useRef<HTMLDivElement>(null);
const [state, setState] = useState<ModalState>("initial");
// const [balanceCode, setBalanceCode] = useState<string>("");
const balanceCodeInputRef = useRef<HTMLInputElement>(null);
const [balanceCodeFocused, setBalanceCodeFocused] = useState<boolean>(false);
const [isBalanceCodeValid, setIsBalanceCodeValid] = useState<boolean>(false);


useEffect(() => {
if (isOpen) {
setState("initial");
}
}, [isOpen]);

useEffect(() => {
if (state === "success") {
const timer = setTimeout(() => {
onClose();
}, 3000);
return () => clearTimeout(timer);
}
}, [state, onClose]);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen && state !== "loading") {
onClose();
}
};

document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, onClose, state]);

useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (
modalRef.current &&
!modalRef.current.contains(e.target as Node) &&
isOpen &&
state !== "loading"
) {
onClose();
}
};

document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isOpen, onClose, state]);

const handleRedeemTransferBalance = async () => {

if (!token) {
toast.error("You must be logged in to redeem a balance code.");
return
}

const balanceCode = balanceCodeInputRef.current?.value?.toString().trim();

if (!balanceCode || balanceCode.length != 26) {
toast.error("Please enter a valid balance code.");
return;
}

setState("loading");

try {
const response = await redeemTransferBalanceCode(balanceCode, token);

if (response.error) {
toast.error(response.error.message);
setState("initial");
} else {
setState("success");
toast.success("Transfer balance redeemed!");
onSuccess();
}
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Failed to redeem balance code"
);
setState("initial");
}
};

if (!isOpen) return null;

return createPortal(
<div className="fixed inset-0 z-50 overflow-y-auto bg-black/70 backdrop-blur-sm flex items-center justify-center p-4 animate-fadeIn">
<div
ref={modalRef}
className="bg-gray-800 rounded-xl shadow-2xl max-w-md w-full mx-auto animate-scaleIn border border-gray-700"
>
<div className="flex items-start justify-between p-6 border-b border-gray-700">
<div className="flex items-center space-x-3">
<div className="flex-shrink-0 p-2 bg-blue-600 rounded-lg">
<TicketCheck size={20} className="text-white" />
</div>
<h3 className="text-lg font-medium text-gray-100">Redeem Transfer Balance Code</h3>
</div>
{state !== "loading" && (
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-200 focus:outline-none focus:text-gray-200 transition-colors p-1 rounded-lg hover:bg-gray-700"
>
<X size={20} />
</button>
)}
</div>

<div className="p-6">
{state === "success" ? (
<div className="text-center py-4">
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-600/20 rounded-full mb-4">
<CheckCircle size={32} className="text-green-400" />
</div>
<h4 className="text-lg font-medium text-green-300 mb-2">
Transfer Balance Code Redeemed Successfully
</h4>
</div>
) : (
<>
<div className="mb-6">

<input
id="redeemBalanceCode"
type="text"
ref={balanceCodeInputRef}
onFocus={() => setBalanceCodeFocused(true)}
onBlur={() => setBalanceCodeFocused(false)}
onChange={(e) =>
setIsBalanceCodeValid(
e.target.value.trim().length == 26,
)
}
className={`w-full px-4 py-3 bg-gray-700 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300 text-white placeholder-gray-400 ${
balanceCodeFocused
? "shadow-lg shadow-blue-500/20"
: ""
} ${
isBalanceCodeValid
? "border-green-500"
: "border-gray-600"
}`}
placeholder="Enter your transfer balance code"
disabled={state === "loading"}
required
/>

<p className="text-gray-500 text-sm mt-3">
Redeeming transfer balance will add data credit to your network.
</p>
</div>

<div className="flex justify-end space-x-3">
<button
onClick={onClose}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-200 rounded-lg transition-colors border border-gray-600"
disabled={state === "loading"}
>
Cancel
</button>
<button
onClick={handleRedeemTransferBalance}
disabled={state === "loading" || !isBalanceCodeValid}
className={`px-4 py-2 bg-blue-600 text-white rounded-lg transition-all duration-200 ${
state === "loading"
? "opacity-70 cursor-not-allowed"
: "hover:bg-blue-700 hover:shadow-lg"
}`}
>
{state === "loading" ? (
<span className="flex items-center justify-center">
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Redeeming...
</span>
) : (
"Redeem"
)}
</button>
</div>
</>
)}
</div>
</div>
</div>,
document.body
);
};

export default RedeemTransferBalanceCodeModal;
Loading