Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ DerivedData
*.ipa
*.xcuserstate
**/.xcode.env.local
**/Settings.bundle/

# Android/IntelliJ
#
Expand All @@ -77,6 +78,7 @@ local.properties
*.keystore
!debug.keystore
.kotlin/
**/config/

# node.js
#
Expand Down
69 changes: 69 additions & 0 deletions Api/ApiContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import axios, { AxiosInstance } from 'axios';
import React, { createContext, ReactNode, useContext } from 'react';
import { AppEnvironment, useAppEnvironment } from '../AppEnvironment';
import { getAuthToken, refreshAuth } from '../Auth/Authentication';

/**
* Context containing API configuration for the app.
* Includes authentication and automatic refresh logic.
*/
export const ApiContext = createContext<AxiosInstance | undefined>(undefined);

/**
* Creates API context in the form of a preconfigured Axios instance.
* Sets base URL to the current app environment, adds interceptors for
* requests to pass in authorization tokens and to retry with refresh tokens
* if any 401 errors occur.
* @param environment AppEnvironment containing the Base URL to use.
* @returns AxiosInstance for the API context.
*/
function createApiContext(environment: AppEnvironment) {
const instance = axios.create({
baseURL: environment.baseUrl,
});

instance.interceptors.request.use(async (config) => {
const token = await getAuthToken(environment);
console.log(config.baseURL);
if (token) {
config.headers.Authorization = `Bearer ${token}`;
console.log(config.headers.Authorization);
}

return config;
});

instance.interceptors.response.use(
(response) => response,
async (error) => {
if (axios.isAxiosError(error) && error.response?.status === 401) {
if (!error.config) return Promise.reject(error);
if (!(await refreshAuth(environment))) return Promise.reject(error);
const token = await getAuthToken(environment);
if (token) {
error.config.headers.Authorization = `Bearer ${token.password}`;
return axios.request(error.config);
}
}
return Promise.reject(error);
},
);
return instance;
}

function ApiContextProvider({ children }: { children: ReactNode }) {
const { environment } = useAppEnvironment();
const apiContext = createApiContext(environment);

return <ApiContext.Provider value={apiContext}>{children}</ApiContext.Provider>;
}

export function useApi() {
const context = useContext(ApiContext);
if (!context) {
throw new Error('useApi must be used within an ApiContextProvider');
}
return context;
}

export default ApiContextProvider;
9 changes: 9 additions & 0 deletions Api/Models/Permission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum Permission {
CREATE_ATTENDANCE = 'create-attendance',
READ_EVENTS = 'read-events',
READ_TEAMS = 'read-teams',
READ_TEAMS_HIDDEN = 'read-teams-hidden',
READ_USERS = 'read-users',
READ_MERCHANDISE = 'read-merchandise',
DISTRIBUTE_SWAG = 'distribute-swag',
}
22 changes: 22 additions & 0 deletions Api/UserApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AxiosInstance } from 'axios';
import { Permission } from './Models/Permission';

export type UserInfo<T extends object = NonNullable<unknown>> = {
id: number;
uid: string;
name: string;
preferred_first_name: string;
allPermissions: Permission[];
} & T;

export async function getUserInfo(api: AxiosInstance): Promise<UserInfo | null> {
try {
const user = await api.get('/api/v1/user');
console.log(user);
//TODO: Incorporate Sentry
return user.data.user;
} catch (error) {
//TODO: incorporate logging
}
return null;
}
22 changes: 9 additions & 13 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
import { NavigationContainer } from '@react-navigation/native';
import React, { createContext } from 'react';
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import ApiContextProvider from './Api/ApiContextProvider';
import { AppEnvironmentProvider } from './AppEnvironment';
import AuthContextProvider from './Auth/AuthContextProvider';
import RootStack from './Navigation/RootStack';
import ThemeProvider from './Themes/ThemeContextProvider';

type AuthContextType = {
authenticated: boolean | null;
setAuthenticated: (u: boolean) => void;
};

export const AuthContext = createContext<AuthContextType | undefined>(undefined);

function App() {
return (
<AppEnvironmentProvider>
<SafeAreaProvider>
<AuthContextProvider>
<ThemeProvider>
<NavigationContainer>
<RootStack />
</NavigationContainer>
</ThemeProvider>
<ApiContextProvider>
<ThemeProvider>
<NavigationContainer>
<RootStack />
</NavigationContainer>
</ThemeProvider>
</ApiContextProvider>
</AuthContextProvider>
</SafeAreaProvider>
</AppEnvironmentProvider>
Expand Down
10 changes: 9 additions & 1 deletion Auth/AuthContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, ReactNode, useEffect, useState } from 'react';
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { useAppEnvironment } from '../AppEnvironment';
import {
AuthenticationState,
Expand Down Expand Up @@ -51,4 +51,12 @@ function AuthContextProvider({ children }: AuthProviderProps) {
);
}

export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthContextProvider');
}
return context;
}

export default AuthContextProvider;
20 changes: 15 additions & 5 deletions Auth/Authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,19 @@ async function storeCredentials(
return store_success && refresh_store_success;
}

/**
* Gets the auth token from Keychain storage.
* @param currentEnvironment Current AppEnvironment.
* @returns Token if exists, null otherwise
*/
export async function getAuthToken(currentEnvironment: AppEnvironment) {
const token = await Keychain.getInternetCredentials(currentEnvironment.baseUrl + ':accessToken');
if (!token) {
return null;
}
return token;
}

/**
* Checks for existence of auth token and whether it is expired.
* @returns whether auth token is currently valid.
Expand All @@ -114,11 +127,8 @@ export async function authTokenIsValid(currentEnvironment: AppEnvironment) {
return false;
}

const currentTime = Date.now();
if (currentTime > expiry) {
return true;
}
return false;
const currentTime = Math.floor(Date.now() / 1000);
return currentTime < expiry;
}

/**
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ npm run android

# OR using Yarn
yarn android

# OR using npx
npx react-native run-android
```

### iOS
Expand All @@ -46,6 +49,11 @@ Then, and every time you update your native dependencies, run:

```sh
bundle exec pod install

# OR manually
cd ios
pod install
cd ..
```

For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html).
Expand All @@ -56,8 +64,26 @@ npm run ios

# OR using Yarn
yarn ios

# OR using npx
npx react-native run-ios
```

If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device.

This is one way to run your app — you can also build it directly from Android Studio or Xcode.


# Legal

This project is open-source and is supported by many open-source libraries.
The app includes a notice of these dependencies which must be updated when a
library is added. Run the below command whenever adding a dependency to update
the OSS notice:

```sh
npx react-native legal-generate

# OR with yarn
yarn react-native legal-generate
```
Loading