mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-13 09:21: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