refactor: Update account-related components and services

Enhanced account-related components and services to include wallet and asset information. Introduced `refreshUserData` function in account handler service to fetch and update user assets. Revised local storage service to include encoding and decoding functionality for secure storage. On the components side, `account-info` and `account-dialog` components have been updated to reflect the new functionalities and data fields. User interface for account information has been improved including details for cryptos and identity information.
This commit is contained in:
Mathis H (Avnyr) 2024-06-14 10:06:57 +02:00
parent a873095099
commit d9858ecae8
Signed by: Mathis
GPG Key ID: DD9E0666A747D126
4 changed files with 216 additions and 64 deletions

View File

@ -4,7 +4,13 @@ import { AccountInfo } from "@/components/account-info";
import { UserDataContext } from "@/components/providers/userdata-provider"; import { UserDataContext } from "@/components/providers/userdata-provider";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import type { IUserData } from "@/interfaces/userdata.interface"; import type { IUserData } from "@/interfaces/userdata.interface";
import {Dispatch, SetStateAction, useContext, useEffect, useState} from "react"; import {
type Dispatch,
type SetStateAction,
useContext,
useEffect,
useState,
} from "react";
export function AccountDialog() { export function AccountDialog() {
const userContext = useContext(UserDataContext); const userContext = useContext(UserDataContext);
@ -12,18 +18,44 @@ export function AccountDialog() {
if (!userContext?.userData) { if (!userContext?.userData) {
userContext?.setUserData({ userContext?.setUserData({
age: 0, firstName: "Mathis",
lastName: "Herriot",
age: 23,
city: "Chambéry", city: "Chambéry",
created_at: "jaj", created_at: "jaj",
dollarAvailables: 34, dollarAvailables: 34,
email: "mherriot@tutanota.com", email: "mherriot@tutanota.com",
id: "", id: "098807e1-6681-407a-a2b4-e6b7e5ec1919",
isActive: false, isActive: true,
lastName: "Herriot",
pseudo: "Avnyr", pseudo: "Avnyr",
roleId: "", roleId: "092fb1eb-4ce4-4e3e-9df1-d00d467c6a43",
updated_at: "", updated_at: "",
firstName: "Mathis", wallet: {
uat: Date.now(),
update_interval: 30_000,
owned_cryptos: [
{
id: "1c237bcc-d2fa-4c4b-9a3a-f0efb4569e1e",
name: "Spectral",
value: 1.61051,
image:
"https://images.unsplash.com/photo-1640833906651-6bd1af7aeea3?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fGNyeXB0b3xlbnwwfHwwfHx8MA%3D%3D",
quantity: 999,
created_at: "2024-06-08T22:42:14.113Z",
updated_at: "2024-06-11T12:48:07.001Z",
},
{
id: "6c0edc95-a968-4c2f-9169-33acfdc73575",
name: "SOL",
value: 30.91268053287073,
image:
"https://images.unsplash.com/photo-1523961131990-5ea7c61b2107?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Nnx8Y3J5cHRvfGVufDB8fDB8fHww",
quantity: 998,
created_at: "2024-06-07T09:30:31.282Z",
updated_at: "2024-06-08T23:44:54.242Z",
},
],
},
}); });
} }
@ -39,7 +71,12 @@ export function AccountDialog() {
return ( return (
<div> <div>
<AccountInfo userData={userContext?.userData as IUserData} setUserData={userContext?.setUserData as Dispatch<SetStateAction<IUserData | undefined>>} /> <AccountInfo
userData={userContext?.userData as IUserData}
setUserData={
userContext?.setUserData as Dispatch<SetStateAction<IUserData | undefined>>
}
/>
</div> </div>
); );
} }

View File

@ -13,8 +13,11 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import type { IUserData } from "@/interfaces/userdata.interface"; import type { IUserData } from "@/interfaces/userdata.interface";
import { Landmark, Unplug, User, Wallet } from "lucide-react"; import { CopyButton } from "@/components/ui/copy-button";
import {Bitcoin, Fingerprint, Landmark, RefreshCw, Unplug, User, Wallet} from "lucide-react";
import type React from "react"; import type React from "react";
import {useState} from "react";
import {refreshUserData} from "@/services/account.handler";
export function AccountInfo({ export function AccountInfo({
userData, userData,
@ -23,6 +26,14 @@ export function AccountInfo({
userData: IUserData; userData: IUserData;
setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>; setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>;
}) { }) {
const [refreshOngoing, setRefreshOngoing] = useState<boolean>(false)
function doingRefresh() {
setRefreshOngoing(true);
refreshUserData().then(()=>setRefreshOngoing(false))
}
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -33,19 +44,59 @@ export function AccountInfo({
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className="sm:max-w-[425px]">
<DialogHeader> <DialogHeader>
<DialogTitle>{`Your account - ${userData.firstName} ${userData.lastName}`}</DialogTitle> <DialogTitle>{`${userData.firstName} ${userData.lastName}`}</DialogTitle>
<DialogDescription>{userData.city}</DialogDescription> <DialogDescription>Your personal account data.</DialogDescription>
</DialogHeader> </DialogHeader>
<div className={"flex flex-col items-center justify-center w-full"}> <div className={"flex flex-col items-center justify-center w-full"}>
<div className={"flex flex-row justify-evenly items-center"}> <div className={"flex flex-col justify-evenly items-center gap-2"}>
<div className={"flex flex-row gap-1 justify-center items-center mx-auto"}> <div
<Landmark /> className={
<p>{userData.dollarAvailables} $</p> "flex flex-col md:flex-row gap-2 justify-center md:justify-evenly items-start md:items-center w-full"
}
>
<div
className={
"flex gap-1 justify-start md:justify-center items-center mx-auto w-full md:w-fit"
}
>
<Landmark />
<p className={"rounded bg-accent text-accent-foreground p-1"}>
{userData.dollarAvailables} $
</p>
</div>
<div
className={
"flex gap-1 justify-start md:justify-center items-center mx-auto w-full md:w-fit"
}
>
<Bitcoin />
<p className={"rounded bg-accent text-accent-foreground p-1"}>
You dont have cryptos.
</p>
</div>
</div>
<div
className={"flex flex-col gap-3 justify-center items-start mx-auto mt-4"}
>
<div className={"flex flex-row text-nowrap flex-nowrap gap-1 text-primary"}>
<Fingerprint />
<h2>Your identity</h2>
</div>
<div
className={
"font-light text-xs md:text-sm flex flex-row items-center justify-start gap-1 bg-accent p-2 rounded"
}
>
<p>{userData.id}</p>
<CopyButton value={userData.id} />
</div>
</div> </div>
<div></div>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter className={"flex justify-evenly items-center w-full gap-2"}>
{!refreshOngoing && <Button variant={"outline"} className={"gap-2 px-2"} onClick={()=>doingRefresh()}>
<RefreshCw />
</Button>}
<Button variant={"secondary"} className={"gap-2 px-2"}> <Button variant={"secondary"} className={"gap-2 px-2"}>
<Wallet /> <Wallet />
<p>My wallet</p> <p>My wallet</p>

View File

@ -4,18 +4,19 @@ import type {
IApiLoginReq, IApiLoginReq,
IApiLoginRes, IApiLoginRes,
IApiRegisterReq, IApiRegisterReq,
IApiRegisterRes, IApiRegisterRes, IApiUserAssetsRes,
} from "@/interfaces/api.interface"; } from "@/interfaces/api.interface";
import type { IUserData } from "@/interfaces/userdata.interface"; import type {IUserData, IUserWallet} from "@/interfaces/userdata.interface";
import ApiRequest from "@/services/apiRequest"; import ApiRequest from "@/services/apiRequest";
import { useEncodedLocalStorage } from "@/services/localStorage"; import { useEncodedLocalStorage } from "@/services/localStorage";
import { createContext, useContext, useState } from "react"; import {createContext, type Dispatch, type SetStateAction, useContext, useEffect, useState} from "react";
import {type ICryptoInUserWalletInfo, ICryptoInWalletInfo} from "@/interfaces/crypto.interface";
const UserDataContext = createContext<IUserData | null>(null);
const [userData, setUserData] = useEncodedLocalStorage<IUserData | null>( const [userData, setUserData] = useEncodedLocalStorage<IUserData | null>(
"user_data", "user_data",
null, null,
); );
const UserDataContext = createContext<readonly [IUserData | null, Dispatch<SetStateAction<IUserData | null>>]>([userData, setUserData]);
//TODO Run register task //TODO Run register task
export async function doRegister( export async function doRegister(
@ -31,7 +32,10 @@ export async function doRegister(
if (ReqRes.data.user) { if (ReqRes.data.user) {
setUserData(ReqRes.data.user); setUserData(ReqRes.data.user);
} }
// biome-ignore lint/complexity/noForEach: <explanation>
ReqRes.data.message?.forEach((err) => console.warn(err)); ReqRes.data.message?.forEach((err) => console.warn(err));
return ReqRes.data; return ReqRes.data;
} catch (error) { } catch (error) {
console.error("Error during registration:", error); console.error("Error during registration:", error);
@ -50,6 +54,8 @@ export async function doLogin(loginData: IApiLoginReq) {
//if (ReqRes.data.user) { //if (ReqRes.data.user) {
// setUserData(ReqRes.data.user) // setUserData(ReqRes.data.user)
//} //}
// biome-ignore lint/complexity/noForEach: <explanation>
ReqRes.data.message?.forEach((err) => console.warn(err)); ReqRes.data.message?.forEach((err) => console.warn(err));
return ReqRes.data; return ReqRes.data;
} catch (err) { } catch (err) {
@ -71,3 +77,38 @@ export function doDisconnect() {
} }
//TODO Run update user data //TODO Run update user data
export async function refreshUserData() {
if (!userData) return;
const isTokenAlive =typeof window !== "undefined" ? window.localStorage.getItem("sub") : null;
if (!isTokenAlive) return;
const _reqData = await ApiRequest.authenticated.get.json<IApiUserAssetsRes>("user/my-assets")
const old_userData = userData
// biome-ignore lint/style/useConst: <explanation>
let new_userData: IUserData = old_userData
let _owned_cryptos: ICryptoInUserWalletInfo[] = []
if (_reqData.data.UserHasCrypto) {
_owned_cryptos = _reqData.data.UserHasCrypto.map((el): ICryptoInUserWalletInfo =>{
return {
...el.Crypto,
owned_amount: el.amount
}
})
}
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
new_userData.wallet = {
uat: Date.now(),
update_interval: 30_000,
owned_cryptos: _owned_cryptos,
}
if (Date.now() >= old_userData.wallet.uat + (new_userData.wallet.update_interval / 1_000)) {
console.log("New update on userData.")
console.trace(new_userData)
setUserData(new_userData)
}
}

View File

@ -39,54 +39,77 @@ export function useLocalStorage<T>(
}, [key, storedValue]); }, [key, storedValue]);
return [storedValue, setStoredValue]; return [storedValue, setStoredValue];
} }
/**
* Custom hook that provides a way to store and retrieve encoded values in local storage.
*
* @template T - The type of the value to be stored.
*
* @param {string} key - The key to be used for storing the encoded value in local storage.
* @param {T} fallbackValue - The fallback value to be used if no value is found in local storage.
*
* @return {readonly [T, React.Dispatch<React.SetStateAction<T>>]} - An array containing the encoded value and a function to update the encoded value.
*/
export function useEncodedLocalStorage<T>(
key: string,
fallbackValue: T,
): readonly [T, React.Dispatch<React.SetStateAction<T>>] {
console.log("Pong !");
const [encodedValue, setEncodedValue] = useState<T>(() => {
const stored = localStorage?.getItem(key);
return stored ? safelyParse(stored, fallbackValue) : fallbackValue;
});
const prevValue = useRef(encodedValue); /**
* Custom hook that provides an encoded version of a state value and a function to update that value.
*
* @template T - The type of the state value.
* @param {string} key - The key under which the state value is stored in the local storage.
* @param {T} initialValue - The initial value of the state.
* @return {[T, React.Dispatch<React.SetStateAction<T>>]} - A tuple containing the encoded state value and the function to update it.
*/
export function useEncodedLocalStorage<T>(key: string, initialValue: T): readonly [T, React.Dispatch<React.SetStateAction<T>>] {
const [encodedState, setEncodedState] = useState<T>(() => getFromLocalStorage(key, initialValue));
useEffect(() => { useEffect(() => {
console.log({ encodedValue }); saveToLocalStorage(key, encodedState);
if (!b64ValEqual(prevValue.current, encodedValue)) { }, [encodedState, key]);
localStorage?.setItem(key, safelyStringify(encodedValue));
}
prevValue.current = encodedValue; // Set ref to current value
}, [key, encodedValue]);
return [encodedValue, setEncodedValue] as const;
function safelyParse(stored: string, fallback: T): T { return [encodedState, setEncodedState] as const;
try { }
return JSON.parse(atob(stored));
} catch {
return fallback;
}
}
function b64ValEqual<T>(v1: T, v2: T): boolean { /**
return btoa(JSON.stringify(v1)) === btoa(JSON.stringify(v2)); * Retrieves a value from the localStorage with the given key.
} *
* @param {string} key - The key to retrieve the value from.
function safelyStringify(value: T): string { * @param {T} initialValue - The initial value to be returned if the value does not exist in the localStorage.
try { * @returns {T} - The retrieved value from the localStorage or the initial value if it does not exist.
return btoa(JSON.stringify(value)); */
} catch { function getFromLocalStorage<T>(key: string, initialValue: T): T {
return btoa(JSON.stringify(fallbackValue)); try {
const localStorageValue = localStorage?.getItem(key);
if (localStorageValue) {
return decodeFromBase64(localStorageValue);
} }
return initialValue;
} catch {
return initialValue;
} }
} }
/**
* Save a value to the local storage.
*
* @param {string} key - The key to store the value.
* @param {*} value - The value to be stored.
* @return {void}
*/
function saveToLocalStorage<T>(key: string, value: T): void {
try {
const encoded = encodeToBase64(value);
localStorage?.setItem(key, encoded);
} catch {}
}
/**
* Decodes a string value from Base64 encoding.
*
* @param {string} value - The Base64 encoded string to decode.
* @template T - The type of the decoded value.
* @returns {T} - The decoded value.
*/
function decodeFromBase64<T>(value: string) {
const decoded = atob(value);
return JSON.parse(decoded) as T;
}
/**
* Encodes the given value to Base64.
*
* @param {T} value - The value to be encoded.
* @returns {string} - The Base64 encoded string.
*/
function encodeToBase64<T>(value: T): string {
const serializedState = JSON.stringify(value);
return btoa(serializedState);
}