diff --git a/apps/mobile/app.json b/apps/mobile/app.json
index 862507ed..108d0c7e 100644
--- a/apps/mobile/app.json
+++ b/apps/mobile/app.json
@@ -43,8 +43,20 @@
"favicon": "./assets/favicon.png"
},
"plugins": [
- "expo-router",
- "expo-secure-store"
+ [
+ "expo-router"
+ ],
+ [
+ "expo-secure-store"
+ ],
+ [
+ "expo-font",
+ {
+ "fonts": [
+ "./assets/fonts/CalSans-SemiBold.otf"
+ ]
+ }
+ ]
],
"extra": {
"router": {
diff --git a/apps/mobile/app/[site_id]/(tabs)/profile/index.tsx b/apps/mobile/app/[site_id]/(tabs)/profile/index.tsx
index 6492aeed..85e86cb9 100644
--- a/apps/mobile/app/[site_id]/(tabs)/profile/index.tsx
+++ b/apps/mobile/app/[site_id]/(tabs)/profile/index.tsx
@@ -19,6 +19,7 @@ import { revokeAsync } from 'expo-auth-session';
import { useContext } from 'react';
import { SiteContext } from '../../_layout';
import * as SecureStore from 'expo-secure-store';
+import { clearDefaultSite, DEFAULT_SITE_KEY, deleteAccessToken, getAccessTokenKey, getRevocationEndpoint } from '@lib/auth';
const SCREEN_OPTIONS = {
title: 'Profile',
@@ -117,11 +118,11 @@ function ListFooterComponent() {
clientId: siteInformation?.client_id || '',
token: tokenParams?.token?.() || ''
}, {
- revocationEndpoint: siteInformation?.url + '/api/method/frappe.integrations.oauth2.revoke_token'
+ revocationEndpoint: getRevocationEndpoint(siteInformation?.url || '')
}).then(result => {
- return SecureStore.deleteItemAsync(`${siteInformation?.sitename}-access-token`)
+ return deleteAccessToken(siteInformation?.sitename || '')
}).then((result) => {
- return AsyncStorage.removeItem('default-site')
+ return clearDefaultSite()
}).then(() => {
router.replace('/landing')
}).catch((error) => {
diff --git a/apps/mobile/app/[site_id]/_layout.tsx b/apps/mobile/app/[site_id]/_layout.tsx
index 8ec95229..17209afa 100644
--- a/apps/mobile/app/[site_id]/_layout.tsx
+++ b/apps/mobile/app/[site_id]/_layout.tsx
@@ -1,12 +1,10 @@
-import { Text } from "@components/nativewindui/Text";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { createContext, useEffect, useState } from "react";
-import { View } from "react-native";
import { SiteInformation } from "../../types/SiteInformation";
-import AsyncStorage from "@react-native-async-storage/async-storage";
-import * as SecureStore from 'expo-secure-store';
import { TokenResponse } from "expo-auth-session";
import { FrappeProvider } from "frappe-react-sdk";
+import FullPageLoader from "@components/layout/FullPageLoader";
+import { getAccessToken, getSiteFromStorage, getTokenEndpoint, storeAccessToken } from "@lib/auth";
export default function SiteLayout() {
@@ -26,19 +24,8 @@ export default function SiteLayout() {
let site_info: SiteInformation | null = null
- AsyncStorage.getItem('sites')
- .then(sites => {
- if (!sites) {
- router.replace('/landing')
-
- // TODO: Show the user a toast saying that the site is not found
-
- return null
- }
-
- const parsedSites: { [key: string]: SiteInformation } = JSON.parse(sites)
- const siteInfo = parsedSites[site_id]
-
+ getSiteFromStorage(site_id)
+ .then(siteInfo => {
if (!siteInfo) {
router.replace('/landing')
@@ -46,7 +33,6 @@ export default function SiteLayout() {
return null
}
-
setSiteInfo(siteInfo)
site_info = siteInfo
@@ -55,7 +41,7 @@ export default function SiteLayout() {
.then((siteInfo: SiteInformation | null) => {
if (!siteInfo) return null
- return SecureStore.getItemAsync(`${site_id}-access-token`)
+ return getAccessToken(siteInfo.sitename)
})
.then(accessToken => {
if (!accessToken) {
@@ -65,16 +51,18 @@ export default function SiteLayout() {
return null
}
- const tokenConfig: TokenResponse = JSON.parse(accessToken)
- let tokenResponse = new TokenResponse(tokenConfig)
+ let tokenResponse = new TokenResponse(accessToken)
if (tokenResponse.shouldRefresh()) {
console.log("Refreshing token")
return tokenResponse.refreshAsync({
clientId: site_info?.client_id || '',
}, {
- tokenEndpoint: site_info?.url + '/api/method/frappe.integrations.oauth2.get_token',
+ tokenEndpoint: getTokenEndpoint(site_info?.url || ''),
+ }).then(async (tokenResponse) => {
+ await storeAccessToken(site_info?.sitename || '', tokenResponse)
+ return tokenResponse
})
} else {
return tokenResponse
@@ -92,12 +80,7 @@ export default function SiteLayout() {
return <>
- {loading ?
-
- {/* TODO: Change this UI */}
- raven
- Setting up your workspace...
- :
+ {loading ? :
+
diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx
new file mode 100644
index 00000000..bfd7ea1a
--- /dev/null
+++ b/apps/mobile/app/index.tsx
@@ -0,0 +1,12 @@
+import { Stack } from 'expo-router';
+import FullPageLoader from '@components/layout/FullPageLoader';
+
+export default function InitialScreen() {
+
+ return (
+ <>
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/apps/mobile/app/landing.tsx b/apps/mobile/app/landing.tsx
index e2f3a90c..527cf844 100644
--- a/apps/mobile/app/landing.tsx
+++ b/apps/mobile/app/landing.tsx
@@ -3,12 +3,12 @@ import { View } from 'react-native';
import { Text } from '@components/nativewindui/Text';
import AddSite from '@components/features/auth/AddSite';
-export default function NotFoundScreen() {
+export default function LandingScreen() {
return (
<>
-
-
- raven
+
+
+ raven
diff --git a/apps/mobile/components/features/auth/AddSite.tsx b/apps/mobile/components/features/auth/AddSite.tsx
index a8de18a8..5b47cea3 100644
--- a/apps/mobile/components/features/auth/AddSite.tsx
+++ b/apps/mobile/components/features/auth/AddSite.tsx
@@ -7,11 +7,10 @@ import { BottomSheetView } from '@gorhom/bottom-sheet'
import { useCallback, useState } from 'react'
import { Alert, View } from 'react-native'
import * as WebBrowser from 'expo-web-browser'
-import * as SecureStore from 'expo-secure-store';
import { CodeChallengeMethod, exchangeCodeAsync, makeRedirectUri, ResponseType, TokenResponse, useAuthRequest } from 'expo-auth-session';
-import AsyncStorage from '@react-native-async-storage/async-storage'
import { router } from 'expo-router'
import { SiteInformation } from '../../../types/SiteInformation'
+import { addSiteToStorage, discovery, setDefaultSite, storeAccessToken } from '@lib/auth'
WebBrowser.maybeCompleteAuthSession();
@@ -87,12 +86,6 @@ const AddSite = (props: Props) => {
)
}
-const discovery = {
- authorizationEndpoint: '/api/method/frappe.integrations.oauth2.authorize',
- tokenEndpoint: '/api/method/frappe.integrations.oauth2.get_token',
- revocationEndpoint: '/api/method/frappe.integrations.oauth2.revoke_token',
-}
-
const SiteAuthFlowSheet = ({ siteInformation, onDismiss }: { siteInformation: SiteInformation, onDismiss: () => void }) => {
const discoveryWithURL = {
@@ -141,8 +134,8 @@ const SiteAuthFlowSheet = ({ siteInformation, onDismiss }: { siteInformation: Si
// 3. Redirect the user to the /[sitename] route
storeAccessToken(siteInformation.sitename, token)
- .then(() => AsyncStorage.mergeItem('sites', JSON.stringify({ [siteInformation.sitename]: siteInformation })))
- .then(() => AsyncStorage.setItem(`default-site`, siteInformation.sitename))
+ .then(() => addSiteToStorage(siteInformation.sitename, siteInformation))
+ .then(() => setDefaultSite(siteInformation.sitename))
.then(() => router.replace(`/${siteInformation.sitename}`))
.then(() => onDismiss())
}
@@ -163,8 +156,4 @@ const SiteAuthFlowSheet = ({ siteInformation, onDismiss }: { siteInformation: Si
}
-const storeAccessToken = (siteName: string, token: TokenResponse) => {
- return SecureStore.setItemAsync(`${siteName}-access-token`, JSON.stringify(token))
-}
-
export default AddSite
\ No newline at end of file
diff --git a/apps/mobile/components/layout/FullPageLoader.tsx b/apps/mobile/components/layout/FullPageLoader.tsx
new file mode 100644
index 00000000..db7944a0
--- /dev/null
+++ b/apps/mobile/components/layout/FullPageLoader.tsx
@@ -0,0 +1,19 @@
+import { Text } from '@components/nativewindui/Text'
+import React from 'react'
+import { View } from 'react-native'
+
+type Props = {
+ title?: string
+ description?: string
+}
+
+const FullPageLoader = ({ title = 'raven', description = 'Setting up your workspace...' }: Props) => {
+ return (
+
+ {title}
+ {description}
+
+ )
+}
+
+export default FullPageLoader
\ No newline at end of file
diff --git a/apps/mobile/lib/auth.ts b/apps/mobile/lib/auth.ts
new file mode 100644
index 00000000..81542477
--- /dev/null
+++ b/apps/mobile/lib/auth.ts
@@ -0,0 +1,161 @@
+import AsyncStorage from "@react-native-async-storage/async-storage"
+import { TokenResponse } from "expo-auth-session"
+import * as SecureStore from 'expo-secure-store'
+import { SiteInformation } from "../types/SiteInformation"
+
+/**
+ * Function to get the access token key for a site
+ *
+ * @param siteName - The name of the site
+ * @returns The access token key for the site - to be used in SecureStore
+ */
+export const getAccessTokenKey = (siteName: string) => `${siteName}-access-token`
+
+/**
+ * Key to store all sites in AsyncStorage
+ */
+export const SITES_KEY = 'sites'
+
+/**
+ * Key to store the default site in AsyncStorage
+ */
+export const DEFAULT_SITE_KEY = 'default-site'
+
+/**
+ * Discovery object for OAuth2 - Frappe OAuth2 endpoints
+ */
+export const discovery = {
+ authorizationEndpoint: '/api/method/frappe.integrations.oauth2.authorize',
+ tokenEndpoint: '/api/method/frappe.integrations.oauth2.get_token',
+ revocationEndpoint: '/api/method/frappe.integrations.oauth2.revoke_token',
+}
+
+/**
+ * Function to get the authorization endpoint for a site
+ *
+ * @param siteURL - The URL of the site
+ * @returns The authorization endpoint for the site
+ */
+export const getAuthorizationEndpoint = (siteURL: string) => `${siteURL}${discovery.authorizationEndpoint}`
+
+/**
+ * Function to get the token endpoint for a site
+ *
+ * @param siteURL - The URL of the site
+ * @returns The token endpoint for the site
+ */
+export const getTokenEndpoint = (siteURL: string) => `${siteURL}${discovery.tokenEndpoint}`
+
+/**
+ * Function to get the revocation endpoint for a site
+ *
+ * @param siteURL - The URL of the site
+ * @returns The revocation endpoint for the site
+ */
+export const getRevocationEndpoint = (siteURL: string) => `${siteURL}${discovery.revocationEndpoint}`
+
+/**
+ * Function to store the access token for a site in SecureStore
+ *
+ * @param siteName - The name of the site
+ * @param token - The access token
+ */
+export const storeAccessToken = (siteName: string, token: TokenResponse) => {
+ return SecureStore.setItemAsync(getAccessTokenKey(siteName), JSON.stringify(token))
+}
+
+/**
+ * Function to delete the access token for a site from SecureStore
+ *
+ * @param siteName - The name of the site
+ */
+export const deleteAccessToken = (siteName: string) => {
+ return SecureStore.deleteItemAsync(getAccessTokenKey(siteName))
+}
+
+/**
+ * Function to get the access token for a site from SecureStore
+ *
+ * @param siteName - The name of the site
+ * @returns The access token for the site
+ */
+export const getAccessToken = async (siteName: string): Promise => {
+ return SecureStore.getItemAsync(getAccessTokenKey(siteName)).then((token) => {
+ return token ? JSON.parse(token) : null
+ })
+}
+
+/**
+ * Function to get all sites from AsyncStorage
+ *
+ * @returns All sites from AsyncStorage
+ */
+export const getSitesFromStorage = async (): Promise> => {
+ return AsyncStorage.getItem(SITES_KEY).then((sites) => {
+ const sitesObj = JSON.parse(sites || '{}')
+ return sitesObj
+ })
+}
+
+/**
+ * Function to get a site from AsyncStorage
+ *
+ * @param siteName - The name of the site
+ * @returns The site from AsyncStorage
+ */
+export const getSiteFromStorage = async (siteName: string): Promise => {
+ return AsyncStorage.getItem(SITES_KEY).then((sites) => {
+ const sitesObj = JSON.parse(sites || '{}')
+ return sitesObj[siteName] || null
+ })
+}
+
+/**
+ * Function to add a site to AsyncStorage
+ *
+ * @param siteName - The name of the site
+ * @param siteInfo - The site information
+ */
+export const addSiteToStorage = async (siteName: string, siteInfo: SiteInformation) => {
+ return AsyncStorage.mergeItem(SITES_KEY, JSON.stringify({ [siteName]: siteInfo }))
+}
+
+/**
+ * Function to remove a site from AsyncStorage
+ *
+ * @param siteName - The name of the site
+ */
+export const removeSiteFromStorage = async (siteName: string) => {
+ return AsyncStorage.getItem(SITES_KEY).then((sites) => {
+ const sitesObj = JSON.parse(sites || '{}')
+ delete sitesObj[siteName]
+ }).then((sitesObj) => {
+ return AsyncStorage.setItem(SITES_KEY, JSON.stringify(sitesObj))
+ })
+}
+
+/**
+ * Function to set the default site in AsyncStorage
+ *
+ * @param siteName - The name of the site
+ */
+export const setDefaultSite = (siteName: string) => {
+ return AsyncStorage.setItem(DEFAULT_SITE_KEY, siteName)
+}
+
+/**
+ * Function to clear the default site from AsyncStorage
+ */
+export const clearDefaultSite = () => {
+ return AsyncStorage.removeItem(DEFAULT_SITE_KEY)
+}
+
+
+/**
+ * Function to get the default site from AsyncStorage
+ *
+ * @returns The default site from AsyncStorage
+ */
+export const getDefaultSite = async (): Promise => {
+ return AsyncStorage.getItem(DEFAULT_SITE_KEY)
+}
\ No newline at end of file
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 75dc31f5..5563559c 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -26,6 +26,7 @@
"expo": "~52.0.18",
"expo-auth-session": "^6.0.1",
"expo-constants": "~17.0.3",
+ "expo-font": "~13.0.1",
"expo-linking": "~7.0.3",
"expo-navigation-bar": "~4.0.6",
"expo-router": "4.0.11",
diff --git a/apps/mobile/tailwind.config.js b/apps/mobile/tailwind.config.js
index 1a31c5f4..b7607236 100644
--- a/apps/mobile/tailwind.config.js
+++ b/apps/mobile/tailwind.config.js
@@ -44,6 +44,9 @@ module.exports = {
borderWidth: {
hairline: hairlineWidth(),
},
+ fontFamily: {
+ "cal-sans": ["CalSans-SemiBold", "sans-serif"],
+ }
},
},
plugins: [],