feat: add payment module
This commit is contained in:
@@ -1,21 +1,25 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { type FunctionComponent, memo, type PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Minimal Controller for managing component state and lifecycle.
|
||||
*/
|
||||
export class Controller<TState extends Record<string, unknown> = {}, TProps extends Record<string, unknown> = {}> {
|
||||
export abstract class Controller<
|
||||
TState extends Record<string, unknown> = {},
|
||||
TProps extends Record<string, unknown> = {},
|
||||
> {
|
||||
state: TState = {} as TState;
|
||||
props: TProps = {} as TProps;
|
||||
|
||||
#resolved = false;
|
||||
#initiated = false;
|
||||
#destroyed = false;
|
||||
|
||||
#setState: (state: Partial<TState>) => void;
|
||||
#setLoading: (state: boolean) => void;
|
||||
|
||||
constructor(setState: (state: Partial<TState>) => void, setLoading: (state: boolean) => void) {
|
||||
declare readonly $state: TState;
|
||||
declare readonly $props: TProps;
|
||||
|
||||
constructor(setState: (state: Partial<TState>) => void) {
|
||||
this.#setState = setState;
|
||||
this.#setLoading = setLoading;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,8 +29,10 @@ export class Controller<TState extends Record<string, unknown> = {}, TProps exte
|
||||
this: TController,
|
||||
setState: any,
|
||||
setLoading: any,
|
||||
setError: any,
|
||||
): InstanceType<TController> {
|
||||
return new this(setState, setLoading) as InstanceType<TController>;
|
||||
// biome-ignore lint/complexity/noThisInStatic: should return new instance of child class
|
||||
return new (this as any)(setState, setLoading, setError) as InstanceType<TController>;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -35,45 +41,35 @@ export class Controller<TState extends Record<string, unknown> = {}, TProps exte
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves the controller with given props.
|
||||
* - First time: Runs onInit() then onResolve()
|
||||
* - Subsequent times: Runs only onResolve()
|
||||
*/
|
||||
async $resolve(props: TProps): Promise<void> {
|
||||
async $init(props: TProps): Promise<void> {
|
||||
if (this.#destroyed === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props = props;
|
||||
let state: Partial<TState> = {};
|
||||
|
||||
try {
|
||||
if (this.#resolved === false) {
|
||||
const initState = await this.onInit();
|
||||
if (initState) {
|
||||
state = { ...state, ...initState };
|
||||
}
|
||||
}
|
||||
const resolveState = await this.onResolve();
|
||||
if (resolveState) {
|
||||
state = { ...state, ...resolveState };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Controller resolve error:", error);
|
||||
throw error;
|
||||
if (this.onInit === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#resolved = true;
|
||||
|
||||
const state = await this.onInit();
|
||||
if (this.#destroyed === false) {
|
||||
this.setState(state);
|
||||
}
|
||||
this.#initiated = true;
|
||||
}
|
||||
|
||||
async $resolve(props: TProps): Promise<void> {
|
||||
if (this.#initiated === false || this.#destroyed === true) {
|
||||
return;
|
||||
}
|
||||
this.props = props;
|
||||
if (this.onResolve === undefined) {
|
||||
return;
|
||||
}
|
||||
const state: Partial<TState> = await this.onResolve();
|
||||
if (this.#destroyed === false) {
|
||||
this.setState({ ...this.state, ...state });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the controller and cleans up resources.
|
||||
*/
|
||||
async $destroy(): Promise<void> {
|
||||
this.#destroyed = true;
|
||||
await this.onDestroy();
|
||||
@@ -86,16 +82,16 @@ export class Controller<TState extends Record<string, unknown> = {}, TProps exte
|
||||
*/
|
||||
|
||||
/**
|
||||
* Called once when the controller is first initialized.
|
||||
* Called every time props change (including first mount).
|
||||
*/
|
||||
async onInit(): Promise<Partial<TState> | void> {
|
||||
async onInit(): Promise<Partial<TState>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every time props change (including first mount).
|
||||
*/
|
||||
async onResolve(): Promise<Partial<TState> | void> {
|
||||
async onResolve(): Promise<Partial<TState>> {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -129,7 +125,6 @@ export class Controller<TState extends Record<string, unknown> = {}, TProps exte
|
||||
}
|
||||
|
||||
this.#setState(this.state);
|
||||
this.#setLoading(false);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -164,59 +159,72 @@ export class Controller<TState extends Record<string, unknown> = {}, TProps exte
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Hook
|
||||
| Component
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* React hook for using a controller.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function LoginView() {
|
||||
* const [state, actions] = useController(LoginController);
|
||||
*
|
||||
* return (
|
||||
* <button onClick={actions.login}>
|
||||
* {state.authenticated ? "Logout" : "Login"}
|
||||
* </button>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useController<TController extends new (...args: any[]) => Controller<any, any>>(
|
||||
export function makeControllerComponent<TController extends new (...args: any[]) => Controller<any, any>>(
|
||||
ControllerClass: TController,
|
||||
props?: InstanceType<TController>["props"],
|
||||
): [InstanceType<TController>["state"], boolean, ControllerActions<InstanceType<TController>>] {
|
||||
const [state, setState] = useState<any>({});
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
Component: FunctionComponent<
|
||||
PropsWithChildren<
|
||||
InstanceType<TController>["$props"] &
|
||||
InstanceType<TController>["$state"] &
|
||||
ControllerActions<InstanceType<TController>>
|
||||
>
|
||||
>,
|
||||
LoadingComponent?: FunctionComponent<PropsWithChildren>,
|
||||
ErrorComponent?: FunctionComponent<PropsWithChildren<{ error: Error }>>,
|
||||
) {
|
||||
const container: FunctionComponent<PropsWithChildren> = (props: any) => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<any>();
|
||||
const [state, setState] = useState<any>();
|
||||
|
||||
const controllerRef = useRef<InstanceType<TController> | null>(null);
|
||||
const actionsRef = useRef<ControllerActions<InstanceType<TController>> | null>(null);
|
||||
const propsRef = useRef(props);
|
||||
const controller = useRef<InstanceType<TController> | null>(null);
|
||||
const actions = useRef<ControllerActions<InstanceType<TController>> | null>(null);
|
||||
|
||||
// Resolve only once after creation
|
||||
useMemo(() => {
|
||||
const instance = (ControllerClass as any).make(setState, setLoading);
|
||||
controllerRef.current = instance;
|
||||
actionsRef.current = instance.toActions();
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: should only execute once
|
||||
useEffect(() => {
|
||||
const instance = (ControllerClass as any).make(setState);
|
||||
|
||||
instance.$resolve(props || {});
|
||||
controller.current = instance;
|
||||
actions.current = instance.toActions();
|
||||
|
||||
return () => {
|
||||
instance.$destroy();
|
||||
};
|
||||
}, [controllerRef]);
|
||||
instance
|
||||
.$init(props || {})
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
setError(error);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
// Resolve on props change
|
||||
useEffect(() => {
|
||||
if (propsRef.current !== props) {
|
||||
propsRef.current = props;
|
||||
controllerRef.current?.$resolve(props || {});
|
||||
return () => {
|
||||
instance.$destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
controller.current?.$resolve(props || {}).catch((error) => {
|
||||
setError(error);
|
||||
});
|
||||
}, [props]);
|
||||
|
||||
if (loading === true || state === undefined) {
|
||||
return LoadingComponent ? <LoadingComponent {...props} /> : null;
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
return [state, loading, actionsRef.current!];
|
||||
if (error !== undefined) {
|
||||
return ErrorComponent ? <ErrorComponent {...props} error={error} /> : null;
|
||||
}
|
||||
|
||||
return <Component {...props} {...state} {...actions.current} />;
|
||||
};
|
||||
|
||||
container.displayName = `${ControllerClass.name}Component`;
|
||||
|
||||
return memo(container);
|
||||
}
|
||||
|
||||
/*
|
||||
Reference in New Issue
Block a user