Mobile app (#3)

* Initial Capacitor Config and plugins

* Add Android project files and resources

* Add local notification scheduling for chores

* Add NotificationAccessSnackbar component for handling notification preferences

* Add capacitor-preferences to Android project

* Update notification Snackbar

* Add local notification scheduling for chores

* Add ionic.config.json file for custom project configuration

* chore: Add environment variables for production deployment

* Add Support for IOS, pass notificaiton token(push notifications)

* Add Capacitor Device support and refactor notification handling

* Refactor GoogleAuth client IDs to use environment variables

* Remove google-services.json to enhance security by eliminating sensitive data from the repository

* Remove environment files to enhance security by eliminating sensitive data from the repository

* Rename project from fe-template to Donetick in ionic.config.json

* Remove GoogleService-Info.plist and Info.plist to enhance security by eliminating sensitive data from the repository

---------

Co-authored-by: Mo Tarbin <mohamad@Mos-MacBook-Pro.local>
This commit is contained in:
Mohamad Tarbin 2024-12-26 02:13:47 -05:00 committed by GitHub
parent 1e7b47e783
commit bcd32a8616
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
130 changed files with 6699 additions and 880 deletions

View file

@ -9,6 +9,8 @@ import { UserContext } from './contexts/UserContext'
import { AuthenticationProvider } from './service/AuthenticationService'
import { GetUserProfile } from './utils/Fetcher'
import { isTokenValid } from './utils/TokenManager'
import { registerCapacitorListeners } from './CapacitorListener'
import {apiManager} from './utils/TokenManager'
const add = className => {
document.getElementById('root').classList.add(className)
@ -20,11 +22,12 @@ const remove = className => {
// TODO: Update the interval to at 60 minutes
const intervalMS = 5 * 60 * 1000 // 5 minutes
function App() {
const queryClient = new QueryClient()
startApiManager()
startOpenReplay()
const queryClient = new QueryClient()
const { mode, systemMode } = useColorScheme()
const [userProfile, setUserProfile] = useState(null)
const [showUpdateSnackbar, setShowUpdateSnackbar] = useState(true)
@ -88,6 +91,7 @@ function App() {
setThemeClass()
}, [mode, systemMode])
useEffect(() => {
registerCapacitorListeners()
if (isTokenValid()) {
if (!userProfile) getUserProfile()
}
@ -132,3 +136,7 @@ const startOpenReplay = () => {
tracker.start()
}
export default App
const startApiManager = () => {
apiManager.init();
}

78
src/CapacitorListener.js Normal file
View file

@ -0,0 +1,78 @@
import { LocalNotifications } from '@capacitor/local-notifications';
import { App as mobileApp } from '@capacitor/app';
import { PushNotifications } from '@capacitor/push-notifications'
import { PutNotificationTarget } from './utils/Fetcher';
import { Capacitor } from '@capacitor/core';
const localNotificationListenerRegistration = () => {
LocalNotifications.addListener('localNotificationReceived', (notification) => {
console.log('Notification received', notification);
});
LocalNotifications.addListener('localNotificationActionPerformed', (event) => {
console.log('Notification action performed', event);
if (event.actionId === 'tap') {
console.log('Notification opened, navigate to chore', event.notification.extra.choreId);
window.location.href = `/chores/${event.notification.extra.choreId}`
}
});
}
const pushNotificationListenerRegistration = () => {
PushNotifications.register();
PushNotifications.addListener('registration', (token) => {
if (Capacitor.isNativePlatform()) {
const type = Capacitor.getPlatform() === 'android' ? 1 : 2; // 1 for android, 2 for ios
PutNotificationTarget(type, token.value).then((response) => {
console.log('Notification target updated', response);
}
).catch((error) => {
console.error('Error updating notification target', error);
}
);
// TODO save the token in preferences and only send it if it has changed:
console.log('Push registration success, token: ' + token.value);
}
}
);
PushNotifications.addListener('registrationError', (error) => {
console.error('Error on registration: ' + JSON.stringify(error));
}
);
PushNotifications.addListener('pushNotificationActionPerformed', fcmEvent => {
if(fcmEvent.actionId === 'tap') {
if (fcmEvent.notification.data.type === 'chore_due') {
window.location.href = `/chores/${fcmEvent.notification.data.choreId}`
}
else {
window.location.href = `/my/chores`
}
}
}
);
}
const registerCapacitorListeners = () => {
if(!Capacitor.isNativePlatform()) {
console.log('Not a native platform, skipping registration of native listeners');
return
}
localNotificationListenerRegistration();
pushNotificationListenerRegistration();
mobileApp.addListener('backButton', ({ canGoBack }) => {
if (canGoBack) {
window.history.back();
} else {
mobileApp.exitApp();
}
});
}
export { registerCapacitorListeners }

View file

@ -21,6 +21,7 @@ import TermsView from '../views/Terms/TermsView'
import TestView from '../views/TestView/Test'
import ThingsHistory from '../views/Things/ThingsHistory'
import ThingsView from '../views/Things/ThingsView'
import LoginSettings from '../views/Authorization/LoginSettings'
const getMainRoute = () => {
if (import.meta.env.VITE_IS_LANDING_DEFAULT === 'true') {
return <Landing />
@ -69,6 +70,10 @@ const Router = createBrowserRouter([
path: '/login',
element: <LoginView />,
},
{
path: '/login/settings',
element: <LoginSettings />,
},
{
path: '/signup',
element: <SignupView />,

View file

@ -1,8 +1,11 @@
import { API_URL } from '../Config'
import { Fetch, HEADERS } from './TokenManager'
import { Fetch, HEADERS, apiManager } from './TokenManager'
const createChore = userID => {
return Fetch(`${API_URL}/chores/`, {
return Fetch(`/chores/`, {
method: 'POST',
headers: HEADERS(),
body: JSON.stringify({
@ -12,7 +15,7 @@ const createChore = userID => {
}
const signUp = (username, password, displayName, email) => {
return fetch(`${API_URL}/auth/`, {
return fetch(`/auth/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -22,7 +25,7 @@ const signUp = (username, password, displayName, email) => {
}
const UpdatePassword = newPassword => {
return fetch(`${API_URL}/users/change_password`, {
return fetch(`/users/change_password`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify({ password: newPassword }),
@ -30,7 +33,8 @@ const UpdatePassword = newPassword => {
}
const login = (username, password) => {
return fetch(`${API_URL}/auth/login`, {
const baseURL = apiManager.getApiURL();
return fetch(`${baseURL}/auth/login`, {
headers: {
'Content-Type': 'application/json',
},
@ -40,13 +44,13 @@ const login = (username, password) => {
}
const GetAllUsers = () => {
return fetch(`${API_URL}/users/`, {
return Fetch(`/users/`, {
method: 'GET',
headers: HEADERS(),
})
}
const GetChoresNew = async () => {
const resp = await Fetch(`${API_URL}/chores/`, {
const resp = await Fetch(`/chores/`, {
method: 'GET',
headers: HEADERS(),
})
@ -54,43 +58,45 @@ const GetChoresNew = async () => {
}
const GetChores = () => {
return Fetch(`${API_URL}/chores/`, {
return Fetch(`/chores/`, {
method: 'GET',
headers: HEADERS(),
})
}
const GetArchivedChores = () => {
return Fetch(`${API_URL}/chores/archived`, {
return Fetch(`/chores/archived`, {
method: 'GET',
headers: HEADERS(),
})
}
const ArchiveChore = id => {
return Fetch(`${API_URL}/chores/${id}/archive`, {
return Fetch(`/chores/${id}/archive`, {
method: 'PUT',
headers: HEADERS(),
})
}
const UnArchiveChore = id => {
return Fetch(`${API_URL}/chores/${id}/unarchive`, {
return Fetch(`/chores/${id}/unarchive`, {
method: 'PUT',
headers: HEADERS(),
})
}
const GetChoreByID = id => {
return Fetch(`${API_URL}/chores/${id}`, {
return Fetch(`/chores/${id}`, {
method: 'GET',
headers: HEADERS(),
})
}
const GetChoreDetailById = id => {
return Fetch(`${API_URL}/chores/${id}/details`, {
return Fetch(`/chores/${id}/details`, {
method: 'GET',
headers: HEADERS(),
})
}
const MarkChoreComplete = (id, note, completedDate) => {
const MarkChoreComplete = (id, note, completedDate, performer) => {
var markChoreURL =`/chores/${id}/do`
const body = {
note,
}
@ -99,8 +105,20 @@ const MarkChoreComplete = (id, note, completedDate) => {
completedDateFormated = `?completedDate=${new Date(
completedDate,
).toISOString()}`
markChoreURL += completedDateFormated
}
return Fetch(`${API_URL}/chores/${id}/do${completedDateFormated}`, {
if (performer) {
body.performer = Number(performer)
if(completedDateFormated === ''){
markChoreURL += `&performer=${performer}`
}
else{
markChoreURL += `?performer=${performer}`
}
}
return Fetch(markChoreURL, {
method: 'POST',
headers: HEADERS(),
body: JSON.stringify(body),
@ -108,7 +126,7 @@ const MarkChoreComplete = (id, note, completedDate) => {
}
const SkipChore = id => {
return Fetch(`${API_URL}/chores/${id}/skip`, {
return Fetch(`/chores/${id}/skip`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -118,7 +136,7 @@ const SkipChore = id => {
}
const UpdateChoreAssignee = (id, assignee) => {
return Fetch(`${API_URL}/chores/${id}/assignee`, {
return Fetch(`/chores/${id}/assignee`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify({ assignee: Number(assignee) }),
@ -126,7 +144,7 @@ const UpdateChoreAssignee = (id, assignee) => {
}
const CreateChore = chore => {
return Fetch(`${API_URL}/chores/`, {
return Fetch(`/chores/`, {
method: 'POST',
headers: HEADERS(),
body: JSON.stringify(chore),
@ -134,14 +152,14 @@ const CreateChore = chore => {
}
const DeleteChore = id => {
return Fetch(`${API_URL}/chores/${id}`, {
return Fetch(`/chores/${id}`, {
method: 'DELETE',
headers: HEADERS(),
})
}
const SaveChore = chore => {
return Fetch(`${API_URL}/chores/`, {
return Fetch(`/chores/`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify(chore),
@ -149,27 +167,27 @@ const SaveChore = chore => {
}
const UpdateChorePriority = (id, priority) => {
return Fetch(`${API_URL}/chores/${id}/priority `, {
return Fetch(`/chores/${id}/priority `, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify({ priority: priority }),
})
}
const GetChoreHistory = choreId => {
return Fetch(`${API_URL}/chores/${choreId}/history`, {
return Fetch(`/chores/${choreId}/history`, {
method: 'GET',
headers: HEADERS(),
})
}
const DeleteChoreHistory = (choreId, id) => {
return Fetch(`${API_URL}/chores/${choreId}/history/${id}`, {
return Fetch(`/chores/${choreId}/history/${id}`, {
method: 'DELETE',
headers: HEADERS(),
})
}
const UpdateChoreHistory = (choreId, id, choreHistory) => {
return Fetch(`${API_URL}/chores/${choreId}/history/${id}`, {
return Fetch(`/chores/${choreId}/history/${id}`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify(choreHistory),
@ -177,49 +195,49 @@ const UpdateChoreHistory = (choreId, id, choreHistory) => {
}
const GetAllCircleMembers = () => {
return Fetch(`${API_URL}/circles/members`, {
return Fetch(`/circles/members`, {
method: 'GET',
headers: HEADERS(),
})
}
const GetUserProfile = () => {
return Fetch(`${API_URL}/users/profile`, {
return Fetch(`/users/profile`, {
method: 'GET',
headers: HEADERS(),
})
}
const GetUserCircle = () => {
return Fetch(`${API_URL}/circles/`, {
return Fetch(`/circles/`, {
method: 'GET',
headers: HEADERS(),
})
}
const JoinCircle = inviteCode => {
return Fetch(`${API_URL}/circles/join?invite_code=${inviteCode}`, {
return Fetch(`/circles/join?invite_code=${inviteCode}`, {
method: 'POST',
headers: HEADERS(),
})
}
const GetCircleMemberRequests = () => {
return Fetch(`${API_URL}/circles/members/requests`, {
return Fetch(`/circles/members/requests`, {
method: 'GET',
headers: HEADERS(),
})
}
const AcceptCircleMemberRequest = id => {
return Fetch(`${API_URL}/circles/members/requests/accept?requestId=${id}`, {
return Fetch(`/circles/members/requests/accept?requestId=${id}`, {
method: 'PUT',
headers: HEADERS(),
})
}
const LeaveCircle = id => {
return Fetch(`${API_URL}/circles/leave?circle_id=${id}`, {
return Fetch(`/circles/leave?circle_id=${id}`, {
method: 'DELETE',
headers: HEADERS(),
})
@ -227,7 +245,7 @@ const LeaveCircle = id => {
const DeleteCircleMember = (circleID, memberID) => {
return Fetch(
`${API_URL}/circles/${circleID}/members/delete?member_id=${memberID}`,
`/circles/${circleID}/members/delete?member_id=${memberID}`,
{
method: 'DELETE',
headers: HEADERS(),
@ -236,7 +254,7 @@ const DeleteCircleMember = (circleID, memberID) => {
}
const UpdateUserDetails = userDetails => {
return Fetch(`${API_URL}/users`, {
return Fetch(`/users`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify(userDetails),
@ -244,7 +262,7 @@ const UpdateUserDetails = userDetails => {
}
const UpdateNotificationTarget = notificationTarget => {
return Fetch(`${API_URL}/users/targets`, {
return Fetch(`/users/targets`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify(notificationTarget),
@ -252,27 +270,27 @@ const UpdateNotificationTarget = notificationTarget => {
}
const GetSubscriptionSession = () => {
return Fetch(API_URL + `/payments/create-subscription`, {
return Fetch(`/payments/create-subscription`, {
method: 'GET',
headers: HEADERS(),
})
}
const CancelSubscription = () => {
return Fetch(API_URL + `/payments/cancel-subscription`, {
return Fetch(`/payments/cancel-subscription`, {
method: 'POST',
headers: HEADERS(),
})
}
const GetThings = () => {
return Fetch(`${API_URL}/things`, {
return Fetch(`/things`, {
method: 'GET',
headers: HEADERS(),
})
}
const CreateThing = thing => {
return Fetch(`${API_URL}/things`, {
return Fetch(`/things`, {
method: 'POST',
headers: HEADERS(),
body: JSON.stringify(thing),
@ -280,7 +298,7 @@ const CreateThing = thing => {
}
const SaveThing = thing => {
return Fetch(`${API_URL}/things`, {
return Fetch(`/things`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify(thing),
@ -288,48 +306,55 @@ const SaveThing = thing => {
}
const UpdateThingState = thing => {
return Fetch(`${API_URL}/things/${thing.id}/state?value=${thing.state}`, {
return Fetch(`/things/${thing.id}/state?value=${thing.state}`, {
method: 'PUT',
headers: HEADERS(),
})
}
const DeleteThing = id => {
return Fetch(`${API_URL}/things/${id}`, {
return Fetch(`/things/${id}`, {
method: 'DELETE',
headers: HEADERS(),
})
}
const GetThingHistory = (id, offset) => {
return Fetch(`${API_URL}/things/${id}/history?offset=${offset}`, {
return Fetch(`/things/${id}/history?offset=${offset}`, {
method: 'GET',
headers: HEADERS(),
})
}
const CreateLongLiveToken = name => {
return Fetch(`${API_URL}/users/tokens`, {
return Fetch(`/users/tokens`, {
method: 'POST',
headers: HEADERS(),
body: JSON.stringify({ name }),
})
}
const DeleteLongLiveToken = id => {
return Fetch(`${API_URL}/users/tokens/${id}`, {
return Fetch(`/users/tokens/${id}`, {
method: 'DELETE',
headers: HEADERS(),
})
}
const GetLongLiveTokens = () => {
return Fetch(`${API_URL}/users/tokens`, {
return Fetch(`/users/tokens`, {
method: 'GET',
headers: HEADERS(),
})
}
const PutNotificationTarget = ( platform, deviceToken) => {
return Fetch(`/users/targets`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify({ platform,deviceToken }),
}
)
}
const CreateLabel = label => {
return Fetch(`${API_URL}/labels`, {
return Fetch(`/labels`, {
method: 'POST',
headers: HEADERS(),
body: JSON.stringify(label),
@ -337,7 +362,7 @@ const CreateLabel = label => {
}
const GetLabels = async () => {
const resp = await Fetch(`${API_URL}/labels`, {
const resp = await Fetch(`/labels`, {
method: 'GET',
headers: HEADERS(),
})
@ -345,19 +370,64 @@ const GetLabels = async () => {
}
const UpdateLabel = label => {
return Fetch(`${API_URL}/labels`, {
return Fetch(`/labels`, {
method: 'PUT',
headers: HEADERS(),
body: JSON.stringify(label),
})
}
const DeleteLabel = id => {
return Fetch(`${API_URL}/labels/${id}`, {
return Fetch(`/labels/${id}`, {
method: 'DELETE',
headers: HEADERS(),
})
}
const ChangePassword = (verifiticationCode, password) => {
const baseURL = apiManager.getApiURL();
return fetch(
`${baseURL}/auth/password?c=${verifiticationCode}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password: password }),
},
)
}
const ResetPassword = email => {
const basedURL = apiManager.getApiURL();
return fetch(`${basedURL}/auth/reset`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: email }),
})
}
const UpdateDueDate = (id, dueDate) => {
return Fetch(`/chores/${chore.id}/dueDate`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dueDate: newDate ? new Date(newDate).toISOString() : null,
UpdatedBy: activeUserId,
}),
})
}
const RefreshToken = () => {
const basedURL = apiManager.getApiURL();
return fetch(basedURL + '/auth/refresh', {
method: 'GET',
headers: HEADERS(),
})
}
export {
AcceptCircleMemberRequest,
ArchiveChore,
@ -369,6 +439,7 @@ export {
CreateThing,
DeleteChore,
DeleteChoreHistory,
ChangePassword,
DeleteCircleMember,
DeleteLabel,
DeleteLongLiveToken,
@ -393,6 +464,9 @@ export {
LeaveCircle,
login,
MarkChoreComplete,
RefreshToken,
ResetPassword,
PutNotificationTarget,
SaveChore,
SaveThing,
signUp,
@ -401,6 +475,7 @@ export {
UpdateChoreAssignee,
UpdateChoreHistory,
UpdateChorePriority,
UpdateDueDate,
UpdateLabel,
UpdateNotificationTarget,
UpdatePassword,

View file

@ -1,21 +1,55 @@
import Cookies from 'js-cookie'
import { API_URL } from '../Config'
import { login, RefreshToken } from './Fetcher'
import { Preferences } from '@capacitor/preferences'
class ApiManager {
constructor(){
this.customServerURL = API_URL
this.initialized = false
}
async init(){
if(this.initialized){
return
}
const { value: serverURL } = await Preferences.get({ key: 'customServerUrl' });
this.customServerURL = serverURL || this.apiURL
this.initialized = true
}
getApiURL(){
return this.customServerURL
}
updateApiURL(url){
this.customServerURL = url
}
}
export const apiManager = new ApiManager();
export function Fetch(url, options) {
if (!isTokenValid()) {
console.log('FETCH: Token is not valid')
console.log(localStorage.getItem('ca_token'))
// store current location in cookie
Cookies.set('ca_redirect', window.location.pathname)
Cookies.set('ca_redirect', window.location.pathname);
// Assuming you have a function isTokenValid() that checks token validity
window.location.href = '/login' // Redirect to login page
window.location.href = '/login'; // Redirect to login page
// return Promise.reject("Token is not valid");
}
if (!options) {
options = {}
options = {};
}
options.headers = { ...options.headers, ...HEADERS() }
options.headers = { ...options.headers, ...HEADERS() };
return fetch(url, options)
const baseURL = apiManager.getApiURL();
const fullURL = `${baseURL}${url}`;
return fetch(fullURL, options);
}
export const HEADERS = () => {
@ -47,10 +81,8 @@ export const isTokenValid = () => {
}
export const refreshAccessToken = () => {
fetch(API_URL + '/auth/refresh', {
method: 'GET',
headers: HEADERS(),
}).then(res => {
RefreshToken()
.then(res => {
if (res.status === 200) {
res.json().then(data => {
localStorage.setItem('ca_token', data.token)

View file

@ -14,6 +14,7 @@ import {
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { API_URL } from './../../Config'
import { ResetPassword } from '../../utils/Fetcher'
const ForgotPasswordView = () => {
const navigate = useNavigate()
@ -43,13 +44,7 @@ const ForgotPasswordView = () => {
}
try {
const response = await fetch(`${API_URL}/auth/reset`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: email }),
})
const response = await ResetPassword(email)
if (response.ok) {
setResetStatusOk(true)

View file

@ -0,0 +1,181 @@
import GoogleIcon from '@mui/icons-material/Google'
import {
Avatar,
Box,
Button,
Container,
Divider,
IconButton,
Input,
Sheet,
Snackbar,
Typography,
} from '@mui/joy'
import React from 'react'
import { LoginSocialGoogle } from 'reactjs-social-login'
import { API_URL, GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
import Logo from '../../Logo'
import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth';
import { Capacitor } from '@capacitor/core'
import { Settings } from '@mui/icons-material'
import { useNavigate } from 'react-router-dom'
import { Preferences } from '@capacitor/preferences'
const LoginSettings = () => {
const [error, setError] = React.useState(null)
const Navigate = useNavigate()
const [serverURL, setServerURL] = React.useState('')
React.useEffect(() => {
Preferences.get({ key: 'customServerUrl' }).then((result) => {
setServerURL(result.value || API_URL)
})
}, [])
const isValidServerURL = () => {
return serverURL.match(/^(http|https):\/\/[^ "]+$/)
}
return (
<Container
component='main'
maxWidth='xs'
>
<Box
sx={{
marginTop: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Sheet
component='form'
sx={{
mt: 1,
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: 2,
borderRadius: '8px',
boxShadow: 'md',
}}
>
<Logo />
<Typography level='h2'>
Done
<span
style={{
color: '#06b6d4',
}}
>
tick
</span>
</Typography>
<Typography level='body2' alignSelf={'start'} mt={4} >
Server URL
</Typography>
<Input
margin='normal'
required
fullWidth
id='serverURL'
name='serverURL'
autoFocus
value={serverURL}
onChange={e => {
setServerURL(e.target.value)
}}
/>
<Typography mt={1} level='body-xs'>
Change the server URL to connect to a different server, such as your own self-hosted Donetick server.
</Typography>
<Typography mt={1} level='body-xs'>
Please ensure to include the protocol (http:// or https://) and the port number if necessary (default Donetick port is 2021).
</Typography>
<Button
fullWidth
size='lg'
variant='solid'
sx={{
width: '100%',
mt: 3,
mb: 2,
border: 'moccasin',
borderRadius: '8px',
}}
onClick={() => {
if (serverURL === '') {
Preferences.set({ key: 'customServerUrl', value: API_URL }).then(() => {
Navigate('/login')
})
return
}
if (!isValidServerURL()){
setError('Invalid server URL')
return
}
Preferences.set({ key: 'customServerUrl', value: serverURL }).then(() => {
Navigate('/login')
}
)
}
}
>
Save
</Button>
<Button
fullWidth
size='lg'
variant='soft'
color='danger'
sx={{
width: '100%',
mb: 2,
border: 'moccasin',
borderRadius: '8px',
}}
onClick={() => {
Preferences.set({ key: 'customServerUrl', value: API_URL }).then(() => {
Navigate('/login')
})
}
}
>
Cancel and Reset
</Button>
</Sheet>
</Box>
<Snackbar
open={error !== null}
onClose={() => setError(null)}
autoHideDuration={3000}
message={error}
>
{error}
</Snackbar>
</Container>
)
}
export default LoginSettings

View file

@ -5,6 +5,7 @@ import {
Button,
Container,
Divider,
IconButton,
Input,
Sheet,
Snackbar,
@ -17,7 +18,13 @@ import { LoginSocialGoogle } from 'reactjs-social-login'
import { API_URL, GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
import { UserContext } from '../../contexts/UserContext'
import Logo from '../../Logo'
import { GetUserProfile } from '../../utils/Fetcher'
import { GetUserProfile, login } from '../../utils/Fetcher'
import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth';
import { Capacitor } from '@capacitor/core'
import { Settings } from '@mui/icons-material'
const LoginView = () => {
const { userProfile, setUserProfile } = React.useContext(UserContext)
const [username, setUsername] = React.useState('')
@ -26,14 +33,8 @@ const LoginView = () => {
const Navigate = useNavigate()
const handleSubmit = async e => {
e.preventDefault()
fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
})
login(username, password)
.then(response => {
if (response.status === 200) {
return response.json().then(data => {
@ -71,7 +72,7 @@ const LoginView = () => {
provider: provider,
token:
data['access_token'] || // data["access_token"] is for Google
data['accessToken'], // data["accessToken"] is for Facebook
data['accessToken'], // data["accessToken"] is for Google Capacitor
data: data,
}),
}).then(response => {
@ -140,6 +141,20 @@ const LoginView = () => {
boxShadow: 'md',
}}
>
<IconButton
// on top right of the screen:
sx={{
position: 'absolute',
top: 2,
right: 2,
color: 'black',
}}
onClick={() => {
Navigate('/login/settings')
}
}
> <Settings /></IconButton>
{/* <img
src='/src/assets/logo.svg'
alt='logo'
@ -285,7 +300,7 @@ const LoginView = () => {
</>
)}
<Divider> or </Divider>
{!Capacitor.isNativePlatform() && (
<Box sx={{ width: '100%' }}>
<LoginSocialGoogle
client_id={GOOGLE_CLIENT_ID}
@ -320,7 +335,32 @@ const LoginView = () => {
</div>
</Button>
</LoginSocialGoogle>
</Box> )}
{Capacitor.isNativePlatform() && (
<Box sx={{ width: '100%' }}>
<Button fullWidth variant='soft' size='lg' sx={{ mt: 3, mb: 2 }}
onClick={()=>{
GoogleAuth.initialize({
clientId: import.meta.env.VITE_APP_GOOGLE_CLIENT_ID,
scopes: ['profile', 'email','openid'],
grantOfflineAccess: true,
});
GoogleAuth.signIn().then((user) => {
console.log("Google user", user);
loggedWithProvider("google", user.authentication)
});
}}>
<div className='flex gap-2'>
<GoogleIcon />
Continue with Google
</div>
</Button>
</Box>
)}
<Button
onClick={() => {

View file

@ -15,6 +15,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'
import { API_URL } from '../../Config'
import Logo from '../../Logo'
import { ChangePassword } from '../../utils/Fetcher'
const UpdatePasswordView = () => {
const navigate = useNavigate()
@ -52,17 +53,8 @@ const UpdatePasswordView = () => {
return
}
try {
const response = await fetch(
`${API_URL}/auth/password?c=${verifiticationCode}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password: password }),
},
)
const response = await ChangePassword(verifiticationCode, password)
if (response.ok) {
setUpdateStatusOk(true)
// wait 3 seconds and then redirect to login:

View file

@ -190,7 +190,7 @@ const ChoreView = () => {
}, 1000)
const id = setTimeout(() => {
MarkChoreComplete(choreId, note, completedDate)
MarkChoreComplete(choreId, note, completedDate, null)
.then(resp => {
if (resp.ok) {
return resp.json().then(data => {

View file

@ -46,10 +46,12 @@ import { API_URL } from '../../Config'
import { UserContext } from '../../contexts/UserContext'
import {
ArchiveChore,
DeleteChore,
MarkChoreComplete,
SkipChore,
UnArchiveChore,
UpdateChoreAssignee,
UpdateDueDate,
} from '../../utils/Fetcher'
import { getTextColorFromBackgroundColor } from '../../utils/LabelColors'
import Priorities from '../../utils/Priorities'
@ -127,11 +129,9 @@ const ChoreCard = ({
cancelText: 'Cancel',
message: 'Are you sure you want to delete this chore?',
onClose: isConfirmed => {
console.log('isConfirmed', isConfirmed)
if (isConfirmed === true) {
Fetch(`${API_URL}/chores/${chore.id}`, {
method: 'DELETE',
}).then(response => {
DeleteChore(chore.id)
.then(response => {
if (response.ok) {
onChoreRemove(chore)
}
@ -181,7 +181,7 @@ const ChoreCard = ({
}, 1000)
const id = setTimeout(() => {
MarkChoreComplete(chore.id)
MarkChoreComplete(chore.id, null, null,null)
.then(resp => {
if (resp.ok) {
return resp.json().then(data => {
@ -206,16 +206,7 @@ const ChoreCard = ({
alert('Please select a performer')
return
}
Fetch(`${API_URL}/chores/${chore.id}/dueDate`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dueDate: newDate ? new Date(newDate).toISOString() : null,
UpdatedBy: activeUserId,
}),
}).then(response => {
UpdateDueDate.then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
@ -230,18 +221,9 @@ const ChoreCard = ({
alert('Please select a performer')
return
}
Fetch(
`${API_URL}/chores/${chore.id}/do?completedDate=${new Date(
newDate,
).toISOString()}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
},
).then(response => {
MarkChoreComplete(chore.id, null, new Date(newDate).toISOString(), null)
.then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
@ -261,15 +243,9 @@ const ChoreCard = ({
})
}
const handleCompleteWithNote = note => {
Fetch(`${API_URL}/chores/${chore.id}/do`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
note: note,
}),
}).then(response => {
MarkChoreComplete(chore.id, note, null, null)
.then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res

View file

@ -0,0 +1,128 @@
import { Capacitor } from '@capacitor/core';
import { LocalNotifications } from '@capacitor/local-notifications';
import { Preferences } from '@capacitor/preferences';
const getNotificationPreferences = async () => {
const ret = await Preferences.get({ key: 'notificationPreferences' });
return JSON.parse(ret.value);
};
const canScheduleNotification = () => {
if (Capacitor.isNativePlatform() === false) {
return false;
}
const notificationPreferences = getNotificationPreferences();
if (notificationPreferences["granted"] === false) {
return false;
}
return true;
}
const scheduleChoreNotification = async (chores, userProfile,allPerformers) => {
// for each chore will create local notification:
const notifications = [];
const now = new Date()
const devicePreferences = await getNotificationPreferences();
for (let i = 0; i < chores.length; i++) {
const chore = chores[i];
const chorePreferences = JSON.parse(chore.notificationMetadata)
if ( chore.notification ===false || chore.nextDueDate === null) {
continue;
}
scheduleDueNotification(chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications)
schedulePreDueNotification(chore, userProfile, allPerformers,chorePreferences, devicePreferences,notifications)
scheduleNaggingNotification(chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications)
}
LocalNotifications.schedule({
notifications,
});
}
const scheduleDueNotification = (chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications) => {
if (devicePreferences['dueNotification'] !== true || chorePreferences['dueDate'] !== true){
return
}
const nextDueDate = new Date(chore.nextDueDate)
const diff = nextDueDate - now
if (diff < 0) {
return
}
const notification = {
title: `${chore.name} is due! 🕒`,
body: userProfile.id === chore.assignedTo ? `It's assigned to you!` : `It is ${allPerformers[chore.assignedTo].name}'s turn`,
id: chore.id,
allowWhileIdle: true,
schedule: {
at: new Date(chore.nextDueDate),
},
extra: {
choreId: chore.id,
},
};
notifications.push(notification);
}
const schedulePreDueNotification = (chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications) => {
if (devicePreferences['preDueNotification'] !== true || chorePreferences['preDue'] !== true){
return
}
const nextDueDate = new Date(chore.nextDueDate)
const diff = nextDueDate - now
if (diff < 0 || userProfile.id !== chore.assignedTo) {
return
}
const notification = {
title: `${chore.name} is due soon! 🕒`,
body: `is due at ${nextDueDate.toLocaleTimeString()}`,
id: chore.id,
allowWhileIdle: true,
schedule: {
// 1 hour before
at: new Date(nextDueDate - 60 * 60 * 1000),
},
extra: {
choreId: chore.id,
},
};
notifications.push(notification);
}
const scheduleNaggingNotification = (chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications) => {
if (devicePreferences['naggingNotification'] === false || chorePreferences.nagging !== true){
return
}
const nextDueDate = new Date(chore.nextDueDate)
const diff = nextDueDate - now
if (diff > 0 || userProfile.id !== chore.assignedTo) {
return
}
const notification = {
title: `${chore.name} is overdue! 🕒`,
body: `❗ It was due at ${nextDueDate.toLocaleTimeString()}`,
id: chore.id,
allowWhileIdle: true,
schedule: {
at: new Date(chore.nextDueDate),
},
extra: {
choreId: chore.id,
},
};
notifications.push(notification);
}
export{ scheduleChoreNotification, canScheduleNotification }

View file

@ -34,6 +34,7 @@ import { useChores } from '../../queries/ChoreQueries'
import {
GetAllUsers,
GetArchivedChores,
GetChores,
GetUserProfile,
} from '../../utils/Fetcher'
import Priorities from '../../utils/Priorities'
@ -42,6 +43,9 @@ import { useLabels } from '../Labels/LabelQueries'
import ChoreCard from './ChoreCard'
import IconButtonWithMenu from './IconButtonWithMenu'
import { canScheduleNotification, scheduleChoreNotification } from './LocalNotificationScheduler'
import NotificationAccessSnackbar from './NotificationAccessSnackbar'
const MyChores = () => {
const { userProfile, setUserProfile } = useContext(UserContext)
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
@ -54,7 +58,6 @@ const MyChores = () => {
const [selectedChoreSection, setSelectedChoreSection] = useState('due_date')
const [openChoreSections, setOpenChoreSections] = useState({})
const [searchTerm, setSearchTerm] = useState('')
const [activeUserId, setActiveUserId] = useState(0)
const [performers, setPerformers] = useState([])
const [anchorEl, setAnchorEl] = useState(null)
const menuRef = useRef(null)
@ -211,26 +214,51 @@ const MyChores = () => {
}
return groups
}
useEffect(() => {
if (userProfile === null) {
GetUserProfile()
.then(response => response.json())
.then(data => {
setUserProfile(data.res)
})
}
GetAllUsers()
.then(response => response.json())
.then(data => {
setPerformers(data.res)
})
Promise.all([GetChores(), GetAllUsers(),GetUserProfile()]).then(responses => {
const [choresResponse, usersResponse, userProfileResponse] = responses;
if (!choresResponse.ok) {
throw new Error(choresResponse.statusText);
}
if (!usersResponse.ok) {
throw new Error(usersResponse.statusText);
}
if (!userProfileResponse.ok) {
throw new Error(userProfileResponse.statusText);
}
Promise.all([choresResponse.json(), usersResponse.json(), userProfileResponse.json()]).then(data => {
const [choresData, usersData, userProfileData] = data;
setUserProfile(userProfileData.res);
choresData.res.sort(choreSorter);
setChores(choresData.res);
setFilteredChores(choresData.res);
setPerformers(usersData.res);
if (canScheduleNotification()) {
scheduleChoreNotification(choresData.res, userProfileData.res, usersData.res);
}
});
const currentUser = JSON.parse(localStorage.getItem('user'))
if (currentUser !== null) {
setActiveUserId(currentUser.id)
}
})
// GetAllUsers()
// .then(response => response.json())
// .then(data => {
// setPerformers(data.res)
// })
// GetUserProfile().then(response => response.json()).then(data => {
// setUserProfile(data.res)
// })
// const currentUser = JSON.parse(localStorage.getItem('user'))
// if (currentUser !== null) {
// setActiveUserId(currentUser.id)
// }
}, [])
useEffect(() => {
@ -391,13 +419,13 @@ const MyChores = () => {
setSearchTerm(term)
setFilteredChores(fuse.search(term).map(result => result.item))
}
if (
userProfile === null ||
userLabelsLoading ||
performers.length === 0 ||
choresLoading
) {
) {
return <LoadingComponent />
}
@ -802,6 +830,7 @@ const MyChores = () => {
>
<Typography level='title-md'>{snackBarMessage}</Typography>
</Snackbar>
<NotificationAccessSnackbar />
</Container>
)
}

View file

@ -0,0 +1,87 @@
import { Capacitor } from '@capacitor/core';
import { Button, Snackbar, Stack, Typography } from '@mui/joy'
import { Preferences } from '@capacitor/preferences';
import { LocalNotifications } from '@capacitor/local-notifications';
import {React, useEffect, useState} from 'react';
const NotificationAccessSnackbar = () => {
const [open, setOpen] = useState(false);
if (!Capacitor.isNativePlatform()) {
return null;
}
const getNotificationPreferences = async () => {
const ret = await Preferences.get({ key: 'notificationPreferences' });
return JSON.parse(ret.value);
};
useEffect(() => {
getNotificationPreferences().then((data) => {
// if optOut is true then don't show the snackbar
if(data?.optOut === true || data?.granted === true) {
return;
}
setOpen(true);
});
}
, []);
return (
<Snackbar
// autoHideDuration={5000}
variant="solid"
color="primary"
size="lg"
invertedColors
open={open}
onClose={() => setOpen(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
sx={(theme) => ({
background: `linear-gradient(45deg, ${theme.palette.primary[600]} 30%, ${theme.palette.primary[500]} 90%})`,
maxWidth: 360,
})}
>
<div>
<Typography level="title-lg">Need Notification?</Typography>
<Typography sx={{ mt: 1, mb: 2 }}>
You need to enable permission to receive notifications, do you want to enable it?
</Typography>
<Stack direction="row" spacing={1}>
<Button variant="solid" color="primary" onClick={() => {
const notificationPreferences = { optOut: false };
LocalNotifications.requestPermissions().then((resp) => {
if (resp.display === 'granted') {
notificationPreferences['granted'] = true;
}
})
Preferences.set({ key: 'notificationPreferences', value: JSON.stringify(notificationPreferences) });
setOpen(false);
}}>
Yes
</Button>
<Button
variant="outlined"
color="primary"
onClick={() => {
const notificationPreferences = { optOut: true };
Preferences.set({ key: 'notificationPreferences', value: JSON.stringify(notificationPreferences) });
setOpen(false);
}}
>
No, Keep it Disabled
</Button>
</Stack>
</div>
</Snackbar>
)
}
export default NotificationAccessSnackbar;

View file

@ -27,7 +27,7 @@ import moment from 'moment'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { API_URL } from '../Config'
import { GetAllUsers } from '../utils/Fetcher'
import { GetAllUsers, GetChores, MarkChoreComplete } from '../utils/Fetcher'
import { Fetch } from '../utils/TokenManager'
import DateModal from './Modals/Inputs/DateModal'
// import moment from 'moment'
@ -98,7 +98,7 @@ const ChoresOverview = () => {
}
useEffect(() => {
// fetch chores:
Fetch(`${API_URL}/chores/`)
GetChores()
.then(response => response.json())
.then(data => {
const filteredData = data.res.filter(
@ -263,9 +263,8 @@ const ChoresOverview = () => {
size='sm'
// sx={{ borderRadius: '50%' }}
onClick={() => {
Fetch(`${API_URL}/chores/${chore.id}/do`, {
method: 'POST',
}).then(response => {
MarkChoreComplete(chore.id,null,null,null)
.then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res
@ -326,14 +325,8 @@ const ChoresOverview = () => {
alert('Please select a performer')
return
}
fetch(
`${API_URL}/chores/${choreId}/do?performer=${activeUserId}&completedDate=${new Date(
date,
).toISOString()}`,
{
method: 'POST',
},
).then(response => {
MarkChoreComplete(choreId, null, date, activeUserId)
.then(response => {
if (response.ok) {
response.json().then(data => {
const newChore = data.res

View file

@ -18,6 +18,7 @@ import { API_URL } from '../../Config'
import {
DeleteChoreHistory,
GetAllCircleMembers,
GetChoreHistory,
UpdateChoreHistory,
} from '../../utils/Fetcher'
import { Fetch } from '../../utils/TokenManager'
@ -40,7 +41,7 @@ const ChoreHistory = () => {
setIsLoading(true) // Start loading
Promise.all([
Fetch(`${API_URL}/chores/${choreId}/history`).then(res => res.json()),
GetChoreHistory(choreId).then(res => res.json()),
GetAllCircleMembers().then(res => res.json()),
])
.then(([historyData, usersData]) => {

View file

@ -1,10 +1,74 @@
import { Button, Divider, Input, Option, Select, Typography } from '@mui/joy'
import { useContext, useState } from 'react'
import { Box, Button, Card, Checkbox, Divider, FormControl, FormHelperText, FormLabel, IconButton, Input, List, ListItem, Option, Select, Snackbar, Switch, Typography } from '@mui/joy'
import React, { useContext, useEffect, useState } from 'react'
import { UserContext } from '../../contexts/UserContext'
import { GetUserProfile, UpdateUserDetails } from '../../utils/Fetcher'
import { Capacitor } from '@capacitor/core'
import { Preferences } from '@capacitor/preferences'
import { LocalNotifications } from '@capacitor/local-notifications'
import { Close } from '@mui/icons-material'
import { PushNotifications } from '@capacitor/push-notifications'
import { UpdateNotificationTarget } from '../../utils/Fetcher'
const NotificationSetting = () => {
const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
const { userProfile, setUserProfile } = useContext(UserContext)
useEffect(() => {
if (!userProfile) {
GetUserProfile().then(resp => {
resp.json().then(data => {
setUserProfile(data.res)
setChatID(data.res.chatID)
})
})
}
}, [])
const getNotificationPreferences = async () => {
const ret = await Preferences.get({ key: 'notificationPreferences' });
return JSON.parse(ret.value);
};
const setNotificationPreferences = async (value) => {
if (value.granted === false){
await Preferences.set({ key: 'notificationPreferences', value: JSON.stringify({ granted: false }) });
return
}
const currentSettings = await getNotificationPreferences();
await Preferences.set({ key: 'notificationPreferences', value: JSON.stringify({ ...currentSettings, ...value }) });
};
const getPushNotificationPreferences = async () => {
const ret = await Preferences.get({ key: 'pushNotificationPreferences' });
return JSON.parse(ret.value);
};
const setPushNotificationPreferences = async (value) => {
await Preferences.set({ key: 'pushNotificationPreferences', value: JSON.stringify(value) });
};
const [deviceNotification, setDeviceNotification] = useState(
false
)
const [dueNotification, setDueNotification] = useState(true)
const [preDueNotification, setPreDueNotification] = useState(false)
const [naggingNotification, setNaggingNotification] = useState(false)
const [pushNotification, setPushNotification] = useState(
false
)
useEffect(() => {
getNotificationPreferences().then((resp) => {
setDeviceNotification(resp.granted)
setDueNotification(resp.dueNotification)
setPreDueNotification(resp.preDueNotification)
setNaggingNotification(resp.naggingNotification)
}
)
getPushNotificationPreferences().then((resp) => {
setPushNotification(resp.granted)
}
)
}, [])
const [notificationTarget, setNotificationTarget] = useState(
userProfile?.notification_target
? String(userProfile.notification_target.type)
@ -57,10 +121,236 @@ const NotificationSetting = () => {
}
return (
<div className='grid gap-4 py-4' id='notifications'>
<Typography level='h3'>Notification Settings</Typography>
<Typography level='h3'>Device Notification</Typography>
<Divider />
<Typography level='body-md'>Manage your notification settings</Typography>
<Typography level='body-md'>Manage your Device Notificaiton</Typography>
<FormControl
orientation="horizontal"
sx={{ width: 400, justifyContent: 'space-between' }}
>
<div>
<FormLabel>Device Notification</FormLabel>
<FormHelperText sx={{ mt: 0 }}>{Capacitor.isNativePlatform()? 'Receive notification on your device when a task is due' : 'This feature is only available on mobile devices'} </FormHelperText>
</div>
<Switch
disabled={!Capacitor.isNativePlatform()}
checked={deviceNotification}
onClick={(event) =>{
event.preventDefault()
if (deviceNotification === false){
LocalNotifications.requestPermissions().then((resp) => {
if (resp.display === 'granted') {
setDeviceNotification(true)
setNotificationPreferences({granted: true})
}
else if (resp.display === 'denied') {
setIsSnackbarOpen(true)
setDeviceNotification(false)
setNotificationPreferences({granted: false})
}
})
}
else{
setDeviceNotification(false)
}
}
}
color={deviceNotification ? 'success' : 'neutral'}
variant={deviceNotification ? 'solid' : 'outlined'}
endDecorator={deviceNotification ? 'On' : 'Off'}
slotProps={{
endDecorator: {
sx: {
minWidth: 24,
},
},
}}
/>
</FormControl>
{deviceNotification && (
<Card>
{[
{
'title': 'Due Date Notification',
'checked': dueNotification,
'set': setDueNotification,
'label': 'Notification when the task is due',
'property': 'dueNotification',
'disabled': false
},
{
'title': 'Pre-Due Date Notification',
'checked': preDueNotification,
'set': setPreDueNotification,
'label': 'Notification a few hours before the task is due',
'property': 'preDueNotification',
'disabled': true
},
{
'title': 'Overdue Notification',
'checked': naggingNotification,
'set': setNaggingNotification,
'label': 'Notification when the task is overdue',
'property': 'naggingNotification',
'disabled': true
}
]
.map(item => (
<FormControl
orientation="horizontal"
sx={{ width: 385, justifyContent: 'space-between' }}
>
<div>
<FormLabel>{item.title}</FormLabel>
<FormHelperText sx={{ mt: 0 }}>{item.label} </FormHelperText>
</div>
<Switch checked={item.checked}
disabled={item.disabled}
onClick={() =>{
setNotificationPreferences({[item.property]: !item.checked})
item.set(!item.checked)
}}
color={item.checked ? 'success' : ''}
variant='solid' endDecorator={item.checked ? 'On' : 'Off'} slotProps={{ endDecorator: { sx: { minWidth: 24 } } }} />
</FormControl>
))}
</Card>
)}
{/* <FormControl
orientation="horizontal"
sx={{ width: 400, justifyContent: 'space-between' }}
>
<div>
<FormLabel>Push Notifications</FormLabel>
<FormHelperText sx={{ mt: 0 }}>{Capacitor.isNativePlatform()? 'Receive push notification when someone complete task' : 'This feature is only available on mobile devices'} </FormHelperText>
</div>
<Switch
disabled={!Capacitor.isNativePlatform()}
checked={pushNotification}
onClick={(event) =>{
event.preventDefault()
if (pushNotification === false){
PushNotifications.requestPermissions().then((resp) => {
console.log("user PushNotifications permission",resp);
if (resp.receive === 'granted') {
setPushNotification(true)
setPushNotificationPreferences({granted: true})
}
if (resp.receive!== 'granted') {
setIsSnackbarOpen(true)
setPushNotification(false)
setPushNotificationPreferences({granted: false})
console.log("User denied permission", resp)
}
})
}
else{
setPushNotification(false)
}
}
}
color={pushNotification ? 'success' : 'neutral'}
variant={pushNotification ? 'solid' : 'outlined'}
endDecorator={pushNotification ? 'On' : 'Off'}
slotProps={{
endDecorator: {
sx: {
minWidth: 24,
},
},
}}
/>
</FormControl> */}
<Button
variant='soft'
color='primary'
sx={{
width: '210px',
mb: 1,
}}
onClick={() => {
// schedule a local notification in 5 seconds
LocalNotifications.schedule({
notifications: [
{
title: 'Task Reminder',
body: 'You have a task due soon',
id: 1,
schedule: { at: new Date(Date.now() + 3000) },
sound: null,
attachments: null,
actionTypeId: '',
extra: null,
},
],
});
}
}>Test Notification </Button>
<Typography level='h3'>Custom Notification</Typography>
<Divider />
<Typography level='body-md'>Notificaiton through other platform like Telegram or Pushover</Typography>
<FormControl
orientation="horizontal"
sx={{ width: 400, justifyContent: 'space-between' }}
>
<div>
<FormLabel>Custom Notification</FormLabel>
<FormHelperText sx={{ mt: 0 }}>Receive notification on other platform</FormHelperText>
</div>
<Switch
checked={chatID !== 0}
onClick={(event) =>{
event.preventDefault()
if (chatID !== 0){
setChatID(0)
}
else{
setChatID('')
UpdateUserDetails({
chatID: Number(0),
}).then(resp => {
resp.json().then(data => {
setUserProfile(data)
})
})
}
setNotificationTarget('0')
handleSave()
}
}
color={chatID!==0 ? 'success' : 'neutral'}
variant={chatID!==0 ? 'solid' : 'outlined'}
endDecorator={chatID!==0 ? 'On' : 'Off'}
slotProps={{
endDecorator: {
sx: {
minWidth: 24,
},
},
}}
/>
</FormControl>
{chatID !== 0&& (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
<Select
value={notificationTarget}
sx={{ maxWidth: '200px' }}
@ -141,6 +431,16 @@ const NotificationSetting = () => {
>
Save
</Button>
</Box>
)}
<Snackbar open={isSnackbarOpen} autoHideDuration={8000} onClose={() => setIsSnackbarOpen(false)} endDecorator={<IconButton size='md' onClick={() => setIsSnackbarOpen(false)}><Close/></IconButton>}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Typography level='title-md'>Permission Denied</Typography>
<Typography level='body-md'>
You have denied the permission to receive notification on this device. Please enable it in your device settings
</Typography>
</div>
</Snackbar>
</div>
)
}

View file

@ -120,6 +120,7 @@ const NavBar = () => {
tick
</span>
</Typography>
</Box>
<Drawer
open={drawerOpen}