@@ -21,6 +21,7 @@ dependencies {
|
||||
implementation project(':capacitor-network')
|
||||
implementation project(':capacitor-preferences')
|
||||
implementation project(':capacitor-push-notifications')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capgo-capacitor-document-scanner')
|
||||
implementation project(':capgo-capacitor-nfc')
|
||||
|
||||
@@ -30,6 +30,17 @@
|
||||
<data android:scheme="donetick" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Verified App Link for Circle invites hosted by Donetick Cloud. -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="app.donetick.com"
|
||||
android:pathPrefix="/circle/join" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- NFC NDEF dispatch: open app directly when a donetick:// tag is scanned -->
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
@@ -38,6 +38,9 @@ project(':capacitor-preferences').projectDir = new File('../node_modules/@capaci
|
||||
include ':capacitor-push-notifications'
|
||||
project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android')
|
||||
|
||||
include ':capacitor-share'
|
||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||
|
||||
include ':capacitor-status-bar'
|
||||
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:app.donetick.com</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.donetick.app</string>
|
||||
|
||||
@@ -23,6 +23,7 @@ def capacitor_pods
|
||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
||||
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
||||
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'CapgoCapacitorDocumentScanner', :path => '../../node_modules/@capgo/capacitor-document-scanner'
|
||||
pod 'CapgoCapacitorNfc', :path => '../../node_modules/@capgo/capacitor-nfc'
|
||||
|
||||
@@ -43,6 +43,8 @@ PODS:
|
||||
- Capacitor
|
||||
- CapacitorPushNotifications (8.1.2):
|
||||
- Capacitor
|
||||
- CapacitorShare (8.0.1):
|
||||
- Capacitor
|
||||
- CapacitorStatusBar (8.0.3):
|
||||
- Capacitor
|
||||
- CapgoCapacitorDocumentScanner (8.4.2):
|
||||
@@ -157,6 +159,7 @@ DEPENDENCIES:
|
||||
- CapacitorPluginSafeArea (from `../../node_modules/capacitor-plugin-safe-area`)
|
||||
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
|
||||
- "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)"
|
||||
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
||||
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
||||
- "CapgoCapacitorDocumentScanner (from `../../node_modules/@capgo/capacitor-document-scanner`)"
|
||||
- "CapgoCapacitorNfc (from `../../node_modules/@capgo/capacitor-nfc`)"
|
||||
@@ -222,6 +225,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@capacitor/preferences"
|
||||
CapacitorPushNotifications:
|
||||
:path: "../../node_modules/@capacitor/push-notifications"
|
||||
CapacitorShare:
|
||||
:path: "../../node_modules/@capacitor/share"
|
||||
CapacitorStatusBar:
|
||||
:path: "../../node_modules/@capacitor/status-bar"
|
||||
CapgoCapacitorDocumentScanner:
|
||||
@@ -256,6 +261,7 @@ SPEC CHECKSUMS:
|
||||
CapacitorPluginSafeArea: 874619c00586248f1694210e72038123d422c2d9
|
||||
CapacitorPreferences: cca2021f386efb75947c850334447d9ff22b14f1
|
||||
CapacitorPushNotifications: 32a7f840815f319fd9ba1c1c9b0b914be9d95237
|
||||
CapacitorShare: 0c58305114538568059bfc07111f22dcb9cb2a82
|
||||
CapacitorStatusBar: eca7bc2b58d9f886f1ef9edb66e57a9b29c121af
|
||||
CapgoCapacitorDocumentScanner: 262bb84b73707f9e2e59071ca58013e3f9acf942
|
||||
CapgoCapacitorNfc: 8ea158143c441e1cf6d231a971e375511558d027
|
||||
@@ -283,6 +289,6 @@ SPEC CHECKSUMS:
|
||||
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: 028fdeb50d56158db0a97459bd577ed700f03c0c
|
||||
PODFILE CHECKSUM: 0170e6b548e03117ef7d6814596b8b140a6acce4
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "donetick",
|
||||
"version": "1.2.33",
|
||||
"version": "1.2.38",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "donetick",
|
||||
"version": "1.2.33",
|
||||
"version": "1.2.38",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@capacitor-community/in-app-review": "^8.0.0",
|
||||
@@ -24,6 +24,7 @@
|
||||
"@capacitor/network": "^8.0.0",
|
||||
"@capacitor/preferences": "^8.0.0",
|
||||
"@capacitor/push-notifications": "^8.0.0",
|
||||
"@capacitor/share": "^8.0.1",
|
||||
"@capacitor/status-bar": "^8.0.0",
|
||||
"@capgo/capacitor-document-scanner": "^8.4.0",
|
||||
"@capgo/capacitor-nfc": "^8.0.0",
|
||||
@@ -2174,6 +2175,15 @@
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/share": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/share/-/share-8.0.1.tgz",
|
||||
"integrity": "sha512-3cSBKBCJVon54rKDROP2rqGyeGks4pBh9TbaEk9S375Kbek/ZHe72N50zIa0Vn9Eac/SuhwgehO/mmA4CsUOiw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/status-bar": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.3.tgz",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"@capacitor/network": "^8.0.0",
|
||||
"@capacitor/preferences": "^8.0.0",
|
||||
"@capacitor/push-notifications": "^8.0.0",
|
||||
"@capacitor/share": "^8.0.1",
|
||||
"@capacitor/status-bar": "^8.0.0",
|
||||
"@capgo/capacitor-document-scanner": "^8.4.0",
|
||||
"@capgo/capacitor-nfc": "^8.0.0",
|
||||
|
||||
11
public/.well-known/apple-app-site-association
Normal file
11
public/.well-known/apple-app-site-association
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"applinks": {
|
||||
"apps": [],
|
||||
"details": [
|
||||
{
|
||||
"appID": "6UJJ78R3BS.com.donetick.app",
|
||||
"paths": ["/circle/join*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
11
public/.well-known/apple-app-site-association.json
Normal file
11
public/.well-known/apple-app-site-association.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"applinks": {
|
||||
"apps": [],
|
||||
"details": [
|
||||
{
|
||||
"appID": "6UJJ78R3BS.com.donetick.app",
|
||||
"paths": ["/circle/join*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
13
public/.well-known/assetlinks.json
Normal file
13
public/.well-known/assetlinks.json
Normal file
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.donetick.app",
|
||||
"sha256_cert_fingerprints": [
|
||||
"EF:45:27:40:A2:D2:11:E4:27:AB:9A:7A:C6:E1:3B:CA:D4:DE:6A:0A:C3:81:05:58:D8:89:F1:FA:4E:CB:44:F3",
|
||||
"67:0B:E5:30:FB:8A:7F:E6:9A:54:51:7F:06:AA:B0:1D:1A:26:61:5B:2A:60:53:4A:31:75:72:DA:F6:FA:EC:5A"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
8
public/_headers
Normal file
8
public/_headers
Normal file
@@ -0,0 +1,8 @@
|
||||
/.well-known/apple-app-site-association
|
||||
Content-Type: application/json
|
||||
|
||||
/.well-known/apple-app-site-association.json
|
||||
Content-Type: application/json
|
||||
|
||||
/.well-known/assetlinks.json
|
||||
Content-Type: application/json
|
||||
1
public/_redirects
Normal file
1
public/_redirects
Normal file
@@ -0,0 +1 @@
|
||||
/.well-known/apple-app-site-association /.well-known/apple-app-site-association.json 200
|
||||
@@ -42,8 +42,8 @@ const AppContent = () => {
|
||||
recordRoute(location.pathname)
|
||||
}, [location.pathname])
|
||||
|
||||
// // First-launch native users see the onboarding flow before anything else.
|
||||
useOnboardingGate()
|
||||
// First-launch native users see the onboarding flow before anything else.
|
||||
const isRedirectingToOnboarding = useOnboardingGate()
|
||||
|
||||
// Initialize status bar with theme-aware configuration
|
||||
useStatusBar()
|
||||
@@ -95,6 +95,8 @@ const AppContent = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [needRefresh])
|
||||
|
||||
if (isRedirectingToOnboarding) return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ImpersonateUserProvider>
|
||||
|
||||
@@ -6,8 +6,11 @@ import { LocalNotifications } from '@capacitor/local-notifications'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import { PushNotifications } from '@capacitor/push-notifications'
|
||||
import { focusManager } from '@tanstack/react-query'
|
||||
|
||||
import { RegisterDeviceToken } from './utils/Fetcher'
|
||||
import { beginOAuthExchange } from './utils/OAuthExchangeState'
|
||||
import { hasSeenOnboarding } from './utils/Onboarding'
|
||||
import { setPendingInvite } from './utils/PendingInvite'
|
||||
|
||||
// React Router navigate(), injected by <App /> once the router is mounted.
|
||||
// Using client-side navigation (instead of window.location.href) avoids a full
|
||||
@@ -63,7 +66,27 @@ const handleNFCChoreDeepLink = (url, isColdStart) => {
|
||||
|
||||
const handleUrlOpen = (url, isColdStart = false) => {
|
||||
console.log('[NFC] handleUrlOpen:', url)
|
||||
if (url.startsWith('donetick://chores/add')) {
|
||||
let parsedUrl
|
||||
try {
|
||||
parsedUrl = new URL(url)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const isCircleInvite =
|
||||
(parsedUrl.protocol === 'donetick:' &&
|
||||
parsedUrl.host === 'circle' &&
|
||||
parsedUrl.pathname === '/join') ||
|
||||
(parsedUrl.protocol === 'https:' && parsedUrl.pathname === '/circle/join')
|
||||
|
||||
if (isCircleInvite) {
|
||||
setPendingInvite(parsedUrl.searchParams.get('code'))
|
||||
const needsOnboarding =
|
||||
!hasSeenOnboarding() && !localStorage.getItem('token')
|
||||
routerNavigate(
|
||||
needsOnboarding ? '/onboarding' : `/circle/join${parsedUrl.search}`,
|
||||
)
|
||||
} else if (url.startsWith('donetick://chores/add')) {
|
||||
// Widget "+" / quick-capture buttons: land on the chore list with the
|
||||
// quick-add modal open (MyChores watches for the add_task param and
|
||||
// consumes it). ?mode=scan|voice opens straight into that capture panel.
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import { hasSeenOnboarding, isNativeApp } from '../utils/Onboarding'
|
||||
import { setPendingInvite } from '../utils/PendingInvite'
|
||||
|
||||
// Routes a first-run user may legitimately be on without having gone through
|
||||
// onboarding: the flow itself, deep-link auth callbacks, and the legal pages
|
||||
@@ -27,16 +28,24 @@ const isAllowed = pathname =>
|
||||
*/
|
||||
const useOnboardingGate = () => {
|
||||
const navigate = useNavigate()
|
||||
const { pathname } = useLocation()
|
||||
const { pathname, search } = useLocation()
|
||||
const isRedirecting =
|
||||
isNativeApp() &&
|
||||
!hasSeenOnboarding() &&
|
||||
!localStorage.getItem('token') &&
|
||||
!isAllowed(pathname)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNativeApp() || hasSeenOnboarding()) return
|
||||
// A signed-in user upgrading from an older build has nothing to onboard to.
|
||||
if (localStorage.getItem('token')) return
|
||||
if (isAllowed(pathname)) return
|
||||
if (!isRedirecting) return
|
||||
|
||||
if (pathname === '/circle/join') {
|
||||
setPendingInvite(new URLSearchParams(search).get('code'))
|
||||
}
|
||||
|
||||
navigate('/onboarding', { replace: true })
|
||||
}, [pathname, navigate])
|
||||
}, [isRedirecting, pathname, search, navigate])
|
||||
|
||||
return isRedirecting
|
||||
}
|
||||
|
||||
export default useOnboardingGate
|
||||
|
||||
@@ -2,9 +2,10 @@ import { App as capacitorApp } from '@capacitor/app'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { commandQueue } from '../utils/CommandQueue'
|
||||
import { offlineDB } from '../utils/OfflineDB'
|
||||
import { isOAuthExchangeInProgress } from '../utils/OAuthExchangeState'
|
||||
import { offlineDB } from '../utils/OfflineDB'
|
||||
import { isOfflineFeatureEnabled } from '../utils/OfflineFeatureToggle'
|
||||
import { syncEngine } from '../utils/SyncEngine'
|
||||
import { networkManager } from './NetworkManager'
|
||||
@@ -91,6 +92,10 @@ export function useSyncOnReconnect() {
|
||||
|
||||
const runSync = async () => {
|
||||
if (!isOfflineFeatureEnabled()) return
|
||||
// Public routes (onboarding, login, signup) have no session to sync.
|
||||
// Calling /sync/changes here returns 401 and the global auth handler
|
||||
// hard-navigates to /login, which reloads the WebView mid-onboarding.
|
||||
if (!localStorage.getItem('token')) return
|
||||
// Skip while the OAuth code exchange is in flight — there's no session
|
||||
// yet, so a sync here just 401s. Note the app-resume listener fires in
|
||||
// the same tick as the deep link, before the route changes, so this has
|
||||
|
||||
@@ -13,7 +13,12 @@ export const joinCirclePath = code =>
|
||||
|
||||
export const setPendingInvite = code => {
|
||||
if (!code) return
|
||||
localStorage.setItem(INVITE_KEY, code)
|
||||
|
||||
try {
|
||||
localStorage.setItem(INVITE_KEY, code)
|
||||
} catch {
|
||||
// The redirect cookie still preserves the invite through authentication.
|
||||
}
|
||||
// Every post-auth landing point (password login, OAuth callback, MFA) already
|
||||
// consumes `ca_redirect`, so reusing it is all the routing this needs.
|
||||
Cookies.set(REDIRECT_COOKIE, joinCirclePath(code), { expires: 1 })
|
||||
@@ -31,10 +36,11 @@ export const clearPendingInvite = () => {
|
||||
try {
|
||||
localStorage.removeItem(INVITE_KEY)
|
||||
} catch {
|
||||
// ignore
|
||||
// Ignore unavailable storage during cleanup.
|
||||
}
|
||||
|
||||
const redirect = Cookies.get(REDIRECT_COOKIE)
|
||||
if (redirect && redirect.startsWith('/circle/join')) {
|
||||
if (redirect?.startsWith('/circle/join')) {
|
||||
Cookies.remove(REDIRECT_COOKIE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ import {
|
||||
import { Box, Button, IconButton, Input, Link, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import useAcknowledgmentModal from '../../hooks/useAcknowledgmentModal'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { GetUserCircle, JoinCircle } from '../../utils/Fetcher'
|
||||
import { haptic } from '../../utils/Onboarding'
|
||||
import { clearPendingInvite, getPendingInvite } from '../../utils/PendingInvite'
|
||||
import { authButtonSx } from '../Authorization/authStyles'
|
||||
import AcknowledgmentModal from '../Modals/Inputs/AcknowledgmentModal'
|
||||
import { CircleVignette } from './OnboardingVignettes'
|
||||
@@ -81,10 +83,11 @@ const CircleSetupView = () => {
|
||||
const navigate = useNavigate()
|
||||
const { showNotification } = useNotification()
|
||||
const { ackModalConfig, showAcknowledgment } = useAcknowledgmentModal()
|
||||
const pendingInvite = getPendingInvite()
|
||||
|
||||
const [mode, setMode] = useState('invite')
|
||||
const [mode, setMode] = useState(pendingInvite ? 'join' : 'invite')
|
||||
const [inviteCode, setInviteCode] = useState(null)
|
||||
const [joinCode, setJoinCode] = useState('')
|
||||
const [joinCode, setJoinCode] = useState(pendingInvite ?? '')
|
||||
const [isJoining, setIsJoining] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -115,6 +118,7 @@ const CircleSetupView = () => {
|
||||
try {
|
||||
const resp = await JoinCircle(joinCode.trim())
|
||||
if (resp.ok) {
|
||||
clearPendingInvite()
|
||||
showAcknowledgment(
|
||||
"Your join request has been sent! The circle owner will need to approve it before you can see their chores. You'll get a notification once you're in.",
|
||||
'Request Sent',
|
||||
@@ -308,7 +312,11 @@ const CircleSetupView = () => {
|
||||
level='body-sm'
|
||||
color='neutral'
|
||||
underline='hover'
|
||||
onClick={() => setMode('invite')}
|
||||
onClick={() => {
|
||||
clearPendingInvite()
|
||||
setJoinCode('')
|
||||
setMode('invite')
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Link>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Delete, Refresh } from '@mui/icons-material'
|
||||
import { Share } from '@capacitor/share'
|
||||
import { CopyAll, Delete, IosShare, Refresh } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -9,15 +10,17 @@ import {
|
||||
Input,
|
||||
Option,
|
||||
Select,
|
||||
Typography
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { apiClient } from '../../utils/ApiClient'
|
||||
import {
|
||||
AcceptCircleMemberRequest,
|
||||
DeleteCircleMember,
|
||||
@@ -115,6 +118,36 @@ const CircleSettings = () => {
|
||||
}
|
||||
}, [circleMembers, userProfile])
|
||||
|
||||
const inviteCode = userCircles[0]?.invite_code
|
||||
const apiURL = new URL(apiClient.getApiURL(), window.location.origin)
|
||||
const inviteOrigin =
|
||||
apiURL.hostname === 'api.donetick.com'
|
||||
? 'https://app.donetick.com'
|
||||
: `${apiURL.origin}${apiURL.pathname.replace(/\/api\/v1\/?$/, '')}`
|
||||
const inviteLink = inviteCode
|
||||
? `${inviteOrigin.replace(/\/$/, '')}/circle/join?code=${encodeURIComponent(inviteCode)}`
|
||||
: ''
|
||||
|
||||
const shareInvite = async () => {
|
||||
const circleName = userCircles[0]?.name || 'my Circle'
|
||||
|
||||
try {
|
||||
await Share.share({
|
||||
title: `Join ${circleName} on Donetick`,
|
||||
text: `I'd like to invite you to join ${circleName} on Donetick.`,
|
||||
url: inviteLink,
|
||||
dialogTitle: 'Share Circle invite',
|
||||
})
|
||||
} catch (error) {
|
||||
if (error?.message?.toLowerCase().includes('cancel')) return
|
||||
await navigator.clipboard.writeText(inviteLink)
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Invite link copied to clipboard',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!userProfile) {
|
||||
return <LoadingComponent />
|
||||
}
|
||||
@@ -128,84 +161,85 @@ const CircleSettings = () => {
|
||||
link below. You'll receive a notification below when someone requests
|
||||
to join your Circle.
|
||||
</Typography>
|
||||
<Typography level='title-sm' mb={-1}>
|
||||
<Box>
|
||||
<Typography level='title-sm' sx={{ mb: 1 }}>
|
||||
{userCircles[0]?.userRole === 'member'
|
||||
? `You part of ${userCircles[0]?.name} `
|
||||
: `You circle code is:`}
|
||||
|
||||
</Typography>
|
||||
<Input
|
||||
value={userCircles[0]?.invite_code}
|
||||
value={inviteCode}
|
||||
disabled
|
||||
size='lg'
|
||||
sx={{
|
||||
width: '220px',
|
||||
width: { xs: '100%', sm: '220px' },
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant='soft'
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(userCircles[0]?.invite_code)
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Code copied to clipboard',
|
||||
})
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
Copy Code
|
||||
</Button>
|
||||
<Button
|
||||
variant='soft'
|
||||
sx={{ ml: 1 }}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
`/circle/join?code=${userCircles[0]?.invite_code}`,
|
||||
)
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Link copied to clipboard',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Copy Link
|
||||
</Button>
|
||||
{userCircles.length > 0 && userCircles[0]?.userRole === 'member' && (
|
||||
<Button
|
||||
color='danger'
|
||||
variant='outlined'
|
||||
sx={{ ml: 1 }}
|
||||
variant='soft'
|
||||
startDecorator={<CopyAll />}
|
||||
onClick={() => {
|
||||
showConfirmation(
|
||||
'Are you sure you want to leave your circle?',
|
||||
'Leave Circle',
|
||||
() => {
|
||||
LeaveCircle(userCircles[0]?.id).then(resp => {
|
||||
if (resp.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Left circle successfully',
|
||||
})
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'Failed to leave circle',
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
'Leave',
|
||||
'Cancel',
|
||||
'danger',
|
||||
)
|
||||
navigator.clipboard.writeText(userCircles[0]?.invite_code)
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Code copied to clipboard',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Leave Circle
|
||||
Copy Code
|
||||
</Button>
|
||||
)}
|
||||
</Typography>
|
||||
<Button
|
||||
variant='soft'
|
||||
disabled={!inviteLink}
|
||||
startDecorator={<IosShare />}
|
||||
onClick={shareInvite}
|
||||
>
|
||||
Share Invite
|
||||
</Button>
|
||||
{userCircles.length > 0 &&
|
||||
userCircles[0]?.userRole === 'member' && (
|
||||
<Button
|
||||
color='danger'
|
||||
variant='outlined'
|
||||
onClick={() => {
|
||||
showConfirmation(
|
||||
'Are you sure you want to leave your circle?',
|
||||
'Leave Circle',
|
||||
() => {
|
||||
LeaveCircle(userCircles[0]?.id).then(resp => {
|
||||
if (resp.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Left circle successfully',
|
||||
})
|
||||
} else {
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'Failed to leave circle',
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
'Leave',
|
||||
'Cancel',
|
||||
'danger',
|
||||
)
|
||||
}}
|
||||
>
|
||||
Leave Circle
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography level='title-md'>Circle Members</Typography>
|
||||
{circleMembers.map(member => (
|
||||
@@ -227,8 +261,7 @@ const CircleSettings = () => {
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography level='body-sm' color='danger'>
|
||||
Request to join{' '}
|
||||
{fmt.date(member.updatedAt)}
|
||||
Request to join {fmt.date(member.updatedAt)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user