mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-14 17:41:20 +00:00
first commit
This commit is contained in:
commit
84218f4fa0
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
1782
package-lock.json
generated
Normal file
1782
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
package.json
Normal file
44
package.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "qapp-core",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Qortal's core React library with global state, UI components, and utilities",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|
||||||
|
"prepare": "npm run build",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"zustand": "^4.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsup": "^7.0.0",
|
||||||
|
"typescript": "^5.2.0",
|
||||||
|
"@types/react": "^18.0.27"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Qortal/qapp-core.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"qortal",
|
||||||
|
"react",
|
||||||
|
"zustand",
|
||||||
|
"global state",
|
||||||
|
"qapp"
|
||||||
|
],
|
||||||
|
"author": "Your Name",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
45
src/context/GlobalProvider.tsx
Normal file
45
src/context/GlobalProvider.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React, { createContext, useContext, useMemo } from "react";
|
||||||
|
import { useAuth, UseAuthProps } from "../hooks/useAuth";
|
||||||
|
|
||||||
|
|
||||||
|
// ✅ Define Global Context Type
|
||||||
|
interface GlobalContextType {
|
||||||
|
auth: ReturnType<typeof useAuth>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Define Config Type for Hook Options
|
||||||
|
interface GlobalProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
config?: {
|
||||||
|
/** Authentication settings. */
|
||||||
|
auth?: UseAuthProps;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Create Context with Proper Type
|
||||||
|
const GlobalContext = createContext<GlobalContextType | null>(null);
|
||||||
|
|
||||||
|
// 🔹 Global Provider (Handles Multiple Hooks)
|
||||||
|
export const GlobalProvider = ({ children, config }: GlobalProviderProps) => {
|
||||||
|
// ✅ Call hooks and pass in options dynamically
|
||||||
|
const auth = useAuth(config?.auth || {});
|
||||||
|
|
||||||
|
|
||||||
|
// ✅ Merge all hooks into a single `contextValue`
|
||||||
|
const contextValue = useMemo(() => ({ auth }), [auth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GlobalContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</GlobalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔹 Hook to Access Global Context
|
||||||
|
export const useGlobal = () => {
|
||||||
|
const context = useContext(GlobalContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useGlobal must be used within a GlobalProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
58
src/global.d.ts
vendored
Normal file
58
src/global.d.ts
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// src/global.d.ts
|
||||||
|
interface QortalRequestOptions {
|
||||||
|
action: string
|
||||||
|
name?: string
|
||||||
|
service?: string
|
||||||
|
data64?: string
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
category?: string
|
||||||
|
tags?: string[]
|
||||||
|
identifier?: string
|
||||||
|
address?: string
|
||||||
|
metaData?: string
|
||||||
|
encoding?: string
|
||||||
|
includeMetadata?: boolean
|
||||||
|
limit?: numebr
|
||||||
|
offset?: number
|
||||||
|
reverse?: boolean
|
||||||
|
resources?: any[]
|
||||||
|
filename?: string
|
||||||
|
list_name?: string
|
||||||
|
item?: string
|
||||||
|
items?: strings[]
|
||||||
|
tag1?: string
|
||||||
|
tag2?: string
|
||||||
|
tag3?: string
|
||||||
|
tag4?: string
|
||||||
|
tag5?: string
|
||||||
|
coin?: string
|
||||||
|
destinationAddress?: string
|
||||||
|
amount?: number
|
||||||
|
blob?: Blob
|
||||||
|
mimeType?: string
|
||||||
|
file?: File
|
||||||
|
encryptedData?: string
|
||||||
|
prefix?: boolean
|
||||||
|
exactMatchNames?: boolean
|
||||||
|
base64?: string
|
||||||
|
groupId?: number
|
||||||
|
isAdmins?: boolean
|
||||||
|
payments?: any[]
|
||||||
|
assetId?: number,
|
||||||
|
publicKeys?: string[],
|
||||||
|
recipient?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
declare function qortalRequest(options: QortalRequestOptions): Promise<any>
|
||||||
|
declare function qortalRequestWithTimeout(
|
||||||
|
options: QortalRequestOptions,
|
||||||
|
time: number,
|
||||||
|
): Promise<any>
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
_qdnBase: any // Replace 'any' with the appropriate type if you know it
|
||||||
|
_qdnTheme: string
|
||||||
|
}
|
||||||
|
}
|
128
src/hooks/useAuth.tsx
Normal file
128
src/hooks/useAuth.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { useAuthStore } from "../state/auth";
|
||||||
|
|
||||||
|
// ✅ Define Types
|
||||||
|
/**
|
||||||
|
* Configuration for balance retrieval behavior.
|
||||||
|
*/
|
||||||
|
export type BalanceSetting =
|
||||||
|
| {
|
||||||
|
/** If `true`, the balance will be fetched only once when the app loads. */
|
||||||
|
onlyOnMount: true;
|
||||||
|
/** `interval` cannot be set when `onlyOnMount` is `true`. */
|
||||||
|
interval?: never;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/** If `false` or omitted, balance will be updated periodically. */
|
||||||
|
onlyOnMount?: false;
|
||||||
|
/** The time interval (in milliseconds) for balance updates. */
|
||||||
|
interval?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export interface UseAuthProps {
|
||||||
|
balanceSetting?: BalanceSetting;
|
||||||
|
/** User will be prompted for authentication on start-up */
|
||||||
|
authenticateOnMount?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = ({ balanceSetting, authenticateOnMount }: UseAuthProps) => {
|
||||||
|
const {
|
||||||
|
address,
|
||||||
|
publicKey,
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
balance,
|
||||||
|
isLoadingUser,
|
||||||
|
isLoadingInitialBalance,
|
||||||
|
errorLoadingUser,
|
||||||
|
setErrorLoadingUser,
|
||||||
|
setIsLoadingUser,
|
||||||
|
setUser,
|
||||||
|
setBalance
|
||||||
|
} = useAuthStore();
|
||||||
|
|
||||||
|
const balanceSetIntervalRef = useRef<null | ReturnType<typeof setInterval>>(null);
|
||||||
|
|
||||||
|
const authenticateUser = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setErrorLoadingUser(null);
|
||||||
|
setIsLoadingUser(true);
|
||||||
|
|
||||||
|
const account = await qortalRequest({
|
||||||
|
action: "GET_USER_ACCOUNT",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (account?.address) {
|
||||||
|
const nameData = await qortalRequest({
|
||||||
|
action: "GET_ACCOUNT_NAMES",
|
||||||
|
address: account.address,
|
||||||
|
});
|
||||||
|
setUser({ ...account, name: nameData[0]?.name || "" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setErrorLoadingUser(
|
||||||
|
error instanceof Error ? error.message : "Unable to authenticate"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingUser(false);
|
||||||
|
}
|
||||||
|
}, [setErrorLoadingUser, setIsLoadingUser, setUser]);
|
||||||
|
|
||||||
|
const getBalance = useCallback(async (address: string) => {
|
||||||
|
try {
|
||||||
|
const response = await qortalRequest({
|
||||||
|
action: "GET_BALANCE",
|
||||||
|
address,
|
||||||
|
});
|
||||||
|
setBalance(Number(response) || 0);
|
||||||
|
} catch (error) {
|
||||||
|
setBalance(0);
|
||||||
|
}
|
||||||
|
}, [setBalance]);
|
||||||
|
|
||||||
|
const balanceSetInterval = useCallback((address: string, interval: number) => {
|
||||||
|
try {
|
||||||
|
if (balanceSetIntervalRef.current) {
|
||||||
|
clearInterval(balanceSetIntervalRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
let isCalling = false;
|
||||||
|
balanceSetIntervalRef.current = setInterval(async () => {
|
||||||
|
if (isCalling) return;
|
||||||
|
isCalling = true;
|
||||||
|
await getBalance(address);
|
||||||
|
isCalling = false;
|
||||||
|
}, interval);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}, [getBalance]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (authenticateOnMount) {
|
||||||
|
authenticateUser();
|
||||||
|
}
|
||||||
|
}, [authenticateOnMount, authenticateUser]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (address && (balanceSetting?.onlyOnMount || (balanceSetting?.interval && !isNaN(balanceSetting?.interval)))) {
|
||||||
|
getBalance(address);
|
||||||
|
}
|
||||||
|
if (address && balanceSetting?.interval !== undefined && !isNaN(balanceSetting.interval)) {
|
||||||
|
balanceSetInterval(address, balanceSetting.interval);
|
||||||
|
}
|
||||||
|
}, [balanceSetting?.onlyOnMount, balanceSetting?.interval, address, getBalance, balanceSetInterval]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
address,
|
||||||
|
publicKey,
|
||||||
|
name,
|
||||||
|
avatarUrl,
|
||||||
|
balance,
|
||||||
|
isLoadingUser,
|
||||||
|
isLoadingInitialBalance,
|
||||||
|
errorLoadingUser,
|
||||||
|
authenticateUser,
|
||||||
|
};
|
||||||
|
};
|
1
src/index.ts
Normal file
1
src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { GlobalProvider, useGlobal } from "./context/GlobalProvider";
|
46
src/state/auth.ts
Normal file
46
src/state/auth.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
// ✅ Define the Auth State Type
|
||||||
|
interface AuthState {
|
||||||
|
/** Qortal address of the visiting user*/
|
||||||
|
address: string | null;
|
||||||
|
/** Qortal public key of the visiting user*/
|
||||||
|
publicKey: string | null;
|
||||||
|
/** Qortal name of the visiting user if they have one*/
|
||||||
|
name: string | null;
|
||||||
|
/** Qortal avatar url. Only exists if the user has a Qortal name. Even though the url exists they might not have an avatar yet.*/
|
||||||
|
avatarUrl: string | null;
|
||||||
|
/** The user's QORT balance*/
|
||||||
|
balance: number | null;
|
||||||
|
isLoadingUser: boolean;
|
||||||
|
isLoadingInitialBalance: boolean;
|
||||||
|
errorLoadingUser: string | null;
|
||||||
|
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
setUser: (user: { address: string; publicKey: string; name?: string }) => void;
|
||||||
|
setBalance: (balance: number) => void;
|
||||||
|
setIsLoadingUser: (loading: boolean) => void;
|
||||||
|
setIsLoadingBalance: (loading: boolean) => void;
|
||||||
|
setErrorLoadingUser: (error: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Typed Zustand Store
|
||||||
|
export const useAuthStore = create<AuthState>((set) => ({
|
||||||
|
address: null,
|
||||||
|
publicKey: null,
|
||||||
|
name: null,
|
||||||
|
avatarUrl: null,
|
||||||
|
balance: null,
|
||||||
|
isLoadingUser: false,
|
||||||
|
isLoadingInitialBalance: false,
|
||||||
|
errorLoadingUser: null,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
setUser: (user) =>
|
||||||
|
set({ address: user.address, publicKey: user.publicKey, name: user.name || null, avatarUrl: !user?.name ? null : `/arbitrary/THUMBNAIL/${user.name}/qortal_avatar?async=true` }),
|
||||||
|
setBalance: (balance) => set({ balance }),
|
||||||
|
setIsLoadingUser: (loading) => set({ isLoadingUser: loading }),
|
||||||
|
setIsLoadingBalance: (loading) => set({ isLoadingInitialBalance: loading }),
|
||||||
|
setErrorLoadingUser: (error) => set({ errorLoadingUser: error }),
|
||||||
|
}));
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src", "src/global.d.ts"]
|
||||||
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user