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:
parent
a873095099
commit
d9858ecae8
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
className={
|
||||||
|
"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 />
|
<Landmark />
|
||||||
<p>{userData.dollarAvailables} $</p>
|
<p className={"rounded bg-accent text-accent-foreground p-1"}>
|
||||||
|
{userData.dollarAvailables} $
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div></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>
|
</div>
|
||||||
<DialogFooter>
|
<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>
|
||||||
|
<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>
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a value from the localStorage with the given key.
|
||||||
|
*
|
||||||
|
* @param {string} key - The key to retrieve the value from.
|
||||||
|
* @param {T} initialValue - The initial value to be returned if the value does not exist in the localStorage.
|
||||||
|
* @returns {T} - The retrieved value from the localStorage or the initial value if it does not exist.
|
||||||
|
*/
|
||||||
|
function getFromLocalStorage<T>(key: string, initialValue: T): T {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(atob(stored));
|
const localStorageValue = localStorage?.getItem(key);
|
||||||
|
if (localStorageValue) {
|
||||||
|
return decodeFromBase64(localStorageValue);
|
||||||
|
}
|
||||||
|
return initialValue;
|
||||||
} catch {
|
} catch {
|
||||||
return fallback;
|
return initialValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function b64ValEqual<T>(v1: T, v2: T): boolean {
|
/**
|
||||||
return btoa(JSON.stringify(v1)) === btoa(JSON.stringify(v2));
|
* Save a value to the local storage.
|
||||||
}
|
*
|
||||||
|
* @param {string} key - The key to store the value.
|
||||||
function safelyStringify(value: T): string {
|
* @param {*} value - The value to be stored.
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
function saveToLocalStorage<T>(key: string, value: T): void {
|
||||||
try {
|
try {
|
||||||
return btoa(JSON.stringify(value));
|
const encoded = encodeToBase64(value);
|
||||||
} catch {
|
localStorage?.setItem(key, encoded);
|
||||||
return btoa(JSON.stringify(fallbackValue));
|
} 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);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user