Skip to content

Commit

Permalink
✨ feat: Add hooks and provider for Appwrite integration
Browse files Browse the repository at this point in the history
Introduces new hooks and a provider component to manage Appwrite authentication and data fetching. The `useFetchData` hook simplifies the process of fetching data from Appwrite, including loading and error state management. The `AppWriteProvider` component provides context for Appwrite authentication, including sign-up, sign-in, and sign-out functionality, as well as user state management.

Changes include:
- `useFetchData` hook for data fetching with loading and error handling.
- `AppWriteProvider` component for managing Appwrite authentication context.
- Exporting new hooks and provider from the main entry point.
- TypeScript definitions for the new hooks and provider.
- Updated `tsconfig.json` to support JSX due to the new React component.
- Adjustments to type definitions to align with the new features.
  • Loading branch information
Gincioks committed May 5, 2024
1 parent bcb968e commit e511fcc
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 0 deletions.
34 changes: 34 additions & 0 deletions src/hooks/useFetchData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useCallback, useEffect, useState } from 'react';

// Assuming the function passed to useAppwrite returns a Promise of type T
const useFetchData = <T,>(function_: () => Promise<T>) => {
// State to store data with initial empty array, typed as T[]
const [data, setData] = useState<T | undefined>();
const [error, setError] = useState<string | undefined>();
// State to store loading status, typed as boolean
const [isLoading, setIsLoading] = useState<boolean>(true);

// Memoize fetchData using useCallback to prevent redefinition unless function_ changes
const fetchData = useCallback(async () => {
setIsLoading(true);
try {
const response = await function_();
setData(response);
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An unexpected error occurred');
} finally {
setIsLoading(false);
}
}, [function_]); // Dependency array includes function_

useEffect(() => {
void fetchData();
}, [fetchData]);

const refetch = async () => fetchData();

// Return type explicitly typed with the structure of the returned object
return { data, isLoading, error, refetch };
};

export default useFetchData;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export { Flag } from './enums/flag';
export { ExecutionMethod } from './enums/execution-method';
export { ImageGravity } from './enums/image-gravity';
export { ImageFormat } from './enums/image-format';
export { useAppWrite, AppWriteProvider } from './lib/AppWriteProvider';
75 changes: 75 additions & 0 deletions src/lib/AppWriteProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import { type Models } from '../index';
import { createContext, type FC, useContext, useEffect, useMemo, useState } from 'react';
import { appWriteAuth } from './app-write-auth';

export type AppwriteConfigType = {
readonly endpoint: string;
readonly platform: string;
readonly projectId: string;
readonly databaseId: string;
readonly userCollectionId: string; // 'users'
};

export type AppWriteProviderProps = {
readonly children: React.ReactNode;
readonly config: AppwriteConfigType;
};

export type AppWriteContextType = {
signUp: (email: string, password: string, username: string) => Promise<Models.Document>;
signIn: (email: string, password: string) => Promise<Models.Session>;
signOut: () => Promise<{}>;
isAuthenticated: boolean;
user: Models.Document | undefined;
isAuthenticationLoading: boolean;
};

const AppWriteContext = createContext<AppWriteContextType | undefined>(undefined);

export const useAppWrite = () => useContext(AppWriteContext);

export const AppWriteProvider: FC<AppWriteProviderProps> = ({ children, config }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [user, setUser] = useState<Models.Document | undefined>(undefined);
const [isAuthenticationLoading, setIsAuthenticationLoading] = useState<boolean>(true);

const { signUp, signIn, signOut, getCurrentUser } = appWriteAuth(config);

const fetchUserData = async () => {
setIsAuthenticationLoading(true);
const userData = await getCurrentUser();
if (userData) {
setIsAuthenticated(true);
setUser(userData);
} else {
setIsAuthenticated(false);
setUser(undefined);
}
setIsAuthenticationLoading(false);
};

const handleSignIn = async (email: string, password: string) => {
const session = await signIn(email, password);
await fetchUserData(); // Refresh user data post-sign-in
return session;
};

const contextValue = useMemo(
() => ({
signUp,
signIn: handleSignIn,
signOut,
isAuthenticated,
user,
isAuthenticationLoading,
}),
[isAuthenticated, user, isAuthenticationLoading]
);

useEffect(() => {
void fetchUserData();
}, []);

return <AppWriteContext.Provider value={contextValue}>{children}</AppWriteContext.Provider>;
};
113 changes: 113 additions & 0 deletions src/lib/app-write-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
Account,
Avatars,
Client,
Databases,
ID,
Query,
} from '../index';

import { type AppwriteConfigType } from './AppWriteProvider';

function handleApiError(error: unknown): Error {
if (error instanceof Error) {
return new Error(error.message);
}
return new Error('An unexpected error occurred');
}

export const appWriteAuth = (config: AppwriteConfigType) => {
const client = new Client();

client.setEndpoint(config.endpoint).setProject(config.projectId).setPlatform(config.platform);

const account = new Account(client);
const avatars = new Avatars(client);
const databases = new Databases(client);

// Register user
const signUp = async (email: string, password: string, username: string) => {
try {
const newAccount = await account.create(ID.unique(), email, password, username);

const avatarUrl = avatars.getInitials(username);

const newUser = await databases.createDocument(
config.databaseId,
config.userCollectionId,
ID.unique(),
{
accountId: newAccount.$id,
email,
username,
avatar: avatarUrl,
}
);

await signIn(email, password);

return newUser;
} catch (error: unknown) {
throw handleApiError(error);
}
};

// Sign In
const signIn = async (email: string, password: string) => {
try {
const session = await account.createEmailPasswordSession(email, password);

return session;
} catch (error) {
throw handleApiError(error);
}
};

// Sign Out
const signOut = async () => {
try {
const session = await account.deleteSession('current');

return session;
} catch (error) {
throw handleApiError(error);
}
};

// Get Account
const getAccount = async () => {
try {
const currentAccount = await account.get();

return currentAccount;
} catch (error) {
throw handleApiError(error);
}
};

// Get Current User
const getCurrentUser = async () => {
try {
const currentAccount = await getAccount();

const currentUser = await databases.listDocuments(
config.databaseId,
config.userCollectionId,
[Query.equal('accountId', currentAccount.$id)]
);

return currentUser.documents[0];
} catch (error) {
console.log(error);
return null;
}
};

return {
signUp,
signIn,
getAccount,
getCurrentUser,
signOut,
};
};
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"esModuleInterop": true,
"inlineSourceMap": false,
"lib": ["ESNext", "DOM"],
"jsx": "react",
"listEmittedFiles": false,
"listFiles": false,
"moduleResolution": "node",
Expand Down
7 changes: 7 additions & 0 deletions types/hooks/useFetchData.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare const useFetchData: <T>(function_: () => Promise<T>) => {
data: T | undefined;
isLoading: boolean;
error: string | undefined;
refetch: () => Promise<void>;
};
export default useFetchData;
1 change: 1 addition & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export { Flag } from './enums/flag';
export { ExecutionMethod } from './enums/execution-method';
export { ImageGravity } from './enums/image-gravity';
export { ImageFormat } from './enums/image-format';
export { useAppWrite, AppWriteProvider } from './lib/AppWriteProvider';
24 changes: 24 additions & 0 deletions types/lib/AppWriteProvider.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { type Models } from '../index';
import { type FC } from 'react';
export declare type AppwriteConfigType = {
readonly endpoint: string;
readonly platform: string;
readonly projectId: string;
readonly databaseId: string;
readonly userCollectionId: string;
};
export declare type AppWriteProviderProps = {
readonly children: React.ReactNode;
readonly config: AppwriteConfigType;
};
export declare type AppWriteContextType = {
signUp: (email: string, password: string, username: string) => Promise<Models.Document>;
signIn: (email: string, password: string) => Promise<Models.Session>;
signOut: () => Promise<{}>;
isAuthenticated: boolean;
user: Models.Document | undefined;
isAuthenticationLoading: boolean;
};
export declare const useAppWrite: () => AppWriteContextType | undefined;
export declare const AppWriteProvider: FC<AppWriteProviderProps>;
8 changes: 8 additions & 0 deletions types/lib/app-write-auth.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type AppwriteConfigType } from './AppWriteProvider';
export declare const appWriteAuth: (config: AppwriteConfigType) => {
signUp: (email: string, password: string, username: string) => Promise<import("../models").Models.Document>;
signIn: (email: string, password: string) => Promise<import("../models").Models.Session>;
getAccount: () => Promise<import("../models").Models.User<import("../models").Models.Preferences>>;
getCurrentUser: () => Promise<import("../models").Models.Document | null>;
signOut: () => Promise<{}>;
};

0 comments on commit e511fcc

Please sign in to comment.