first commit

This commit is contained in:
PhilReact 2025-03-10 17:15:13 +02:00
commit 84218f4fa0
9 changed files with 2143 additions and 0 deletions

24
.gitignore vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

44
package.json Normal file
View 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"
}

View 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
View 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
View 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
View File

@ -0,0 +1 @@
export { GlobalProvider, useGlobal } from "./context/GlobalProvider";

46
src/state/auth.ts Normal file
View 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
View 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"]
}