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:
parent
1e7b47e783
commit
bcd32a8616
130 changed files with 6699 additions and 880 deletions
12
src/App.jsx
12
src/App.jsx
|
@ -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
78
src/CapacitorListener.js
Normal 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 }
|
|
@ -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 />,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
181
src/views/Authorization/LoginSettings.jsx
Normal file
181
src/views/Authorization/LoginSettings.jsx
Normal 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
|
|
@ -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={() => {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
|
|
128
src/views/Chores/LocalNotificationScheduler.js
Normal file
128
src/views/Chores/LocalNotificationScheduler.js
Normal 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 }
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
87
src/views/Chores/NotificationAccessSnackbar.jsx
Normal file
87
src/views/Chores/NotificationAccessSnackbar.jsx
Normal 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;
|
|
@ -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
|
||||
|
|
|
@ -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]) => {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -120,6 +120,7 @@ const NavBar = () => {
|
|||
tick✓
|
||||
</span>
|
||||
</Typography>
|
||||
|
||||
</Box>
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue