Add Capacitor Share integration and implement Circle invite sharing feature
This commit is contained in:
@@ -21,6 +21,7 @@ dependencies {
|
|||||||
implementation project(':capacitor-network')
|
implementation project(':capacitor-network')
|
||||||
implementation project(':capacitor-preferences')
|
implementation project(':capacitor-preferences')
|
||||||
implementation project(':capacitor-push-notifications')
|
implementation project(':capacitor-push-notifications')
|
||||||
|
implementation project(':capacitor-share')
|
||||||
implementation project(':capacitor-status-bar')
|
implementation project(':capacitor-status-bar')
|
||||||
implementation project(':capgo-capacitor-document-scanner')
|
implementation project(':capgo-capacitor-document-scanner')
|
||||||
implementation project(':capgo-capacitor-nfc')
|
implementation project(':capgo-capacitor-nfc')
|
||||||
|
|||||||
@@ -30,6 +30,17 @@
|
|||||||
<data android:scheme="donetick" />
|
<data android:scheme="donetick" />
|
||||||
</intent-filter>
|
</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 -->
|
<!-- NFC NDEF dispatch: open app directly when a donetick:// tag is scanned -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
<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'
|
include ':capacitor-push-notifications'
|
||||||
project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android')
|
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'
|
include ':capacitor-status-bar'
|
||||||
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>aps-environment</key>
|
<key>aps-environment</key>
|
||||||
<string>development</string>
|
<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>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.com.donetick.app</string>
|
<string>group.com.donetick.app</string>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ def capacitor_pods
|
|||||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
||||||
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
||||||
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
|
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
|
||||||
|
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||||
pod 'CapgoCapacitorDocumentScanner', :path => '../../node_modules/@capgo/capacitor-document-scanner'
|
pod 'CapgoCapacitorDocumentScanner', :path => '../../node_modules/@capgo/capacitor-document-scanner'
|
||||||
pod 'CapgoCapacitorNfc', :path => '../../node_modules/@capgo/capacitor-nfc'
|
pod 'CapgoCapacitorNfc', :path => '../../node_modules/@capgo/capacitor-nfc'
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ PODS:
|
|||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorPushNotifications (8.1.2):
|
- CapacitorPushNotifications (8.1.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
|
- CapacitorShare (8.0.1):
|
||||||
|
- Capacitor
|
||||||
- CapacitorStatusBar (8.0.3):
|
- CapacitorStatusBar (8.0.3):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
- CapgoCapacitorDocumentScanner (8.4.2):
|
- CapgoCapacitorDocumentScanner (8.4.2):
|
||||||
@@ -157,6 +159,7 @@ DEPENDENCIES:
|
|||||||
- CapacitorPluginSafeArea (from `../../node_modules/capacitor-plugin-safe-area`)
|
- CapacitorPluginSafeArea (from `../../node_modules/capacitor-plugin-safe-area`)
|
||||||
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
|
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
|
||||||
- "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)"
|
- "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)"
|
||||||
|
- "CapacitorShare (from `../../node_modules/@capacitor/share`)"
|
||||||
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
||||||
- "CapgoCapacitorDocumentScanner (from `../../node_modules/@capgo/capacitor-document-scanner`)"
|
- "CapgoCapacitorDocumentScanner (from `../../node_modules/@capgo/capacitor-document-scanner`)"
|
||||||
- "CapgoCapacitorNfc (from `../../node_modules/@capgo/capacitor-nfc`)"
|
- "CapgoCapacitorNfc (from `../../node_modules/@capgo/capacitor-nfc`)"
|
||||||
@@ -222,6 +225,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../node_modules/@capacitor/preferences"
|
:path: "../../node_modules/@capacitor/preferences"
|
||||||
CapacitorPushNotifications:
|
CapacitorPushNotifications:
|
||||||
:path: "../../node_modules/@capacitor/push-notifications"
|
:path: "../../node_modules/@capacitor/push-notifications"
|
||||||
|
CapacitorShare:
|
||||||
|
:path: "../../node_modules/@capacitor/share"
|
||||||
CapacitorStatusBar:
|
CapacitorStatusBar:
|
||||||
:path: "../../node_modules/@capacitor/status-bar"
|
:path: "../../node_modules/@capacitor/status-bar"
|
||||||
CapgoCapacitorDocumentScanner:
|
CapgoCapacitorDocumentScanner:
|
||||||
@@ -256,6 +261,7 @@ SPEC CHECKSUMS:
|
|||||||
CapacitorPluginSafeArea: 874619c00586248f1694210e72038123d422c2d9
|
CapacitorPluginSafeArea: 874619c00586248f1694210e72038123d422c2d9
|
||||||
CapacitorPreferences: cca2021f386efb75947c850334447d9ff22b14f1
|
CapacitorPreferences: cca2021f386efb75947c850334447d9ff22b14f1
|
||||||
CapacitorPushNotifications: 32a7f840815f319fd9ba1c1c9b0b914be9d95237
|
CapacitorPushNotifications: 32a7f840815f319fd9ba1c1c9b0b914be9d95237
|
||||||
|
CapacitorShare: 0c58305114538568059bfc07111f22dcb9cb2a82
|
||||||
CapacitorStatusBar: eca7bc2b58d9f886f1ef9edb66e57a9b29c121af
|
CapacitorStatusBar: eca7bc2b58d9f886f1ef9edb66e57a9b29c121af
|
||||||
CapgoCapacitorDocumentScanner: 262bb84b73707f9e2e59071ca58013e3f9acf942
|
CapgoCapacitorDocumentScanner: 262bb84b73707f9e2e59071ca58013e3f9acf942
|
||||||
CapgoCapacitorNfc: 8ea158143c441e1cf6d231a971e375511558d027
|
CapgoCapacitorNfc: 8ea158143c441e1cf6d231a971e375511558d027
|
||||||
@@ -283,6 +289,6 @@ SPEC CHECKSUMS:
|
|||||||
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
|
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
|
||||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||||
|
|
||||||
PODFILE CHECKSUM: 028fdeb50d56158db0a97459bd577ed700f03c0c
|
PODFILE CHECKSUM: 0170e6b548e03117ef7d6814596b8b140a6acce4
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"version": "1.2.33",
|
"version": "1.2.38",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "donetick",
|
"name": "donetick",
|
||||||
"version": "1.2.33",
|
"version": "1.2.38",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/in-app-review": "^8.0.0",
|
"@capacitor-community/in-app-review": "^8.0.0",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"@capacitor/network": "^8.0.0",
|
"@capacitor/network": "^8.0.0",
|
||||||
"@capacitor/preferences": "^8.0.0",
|
"@capacitor/preferences": "^8.0.0",
|
||||||
"@capacitor/push-notifications": "^8.0.0",
|
"@capacitor/push-notifications": "^8.0.0",
|
||||||
|
"@capacitor/share": "^8.0.1",
|
||||||
"@capacitor/status-bar": "^8.0.0",
|
"@capacitor/status-bar": "^8.0.0",
|
||||||
"@capgo/capacitor-document-scanner": "^8.4.0",
|
"@capgo/capacitor-document-scanner": "^8.4.0",
|
||||||
"@capgo/capacitor-nfc": "^8.0.0",
|
"@capgo/capacitor-nfc": "^8.0.0",
|
||||||
@@ -2174,6 +2175,15 @@
|
|||||||
"@capacitor/core": ">=8.0.0"
|
"@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": {
|
"node_modules/@capacitor/status-bar": {
|
||||||
"version": "8.0.3",
|
"version": "8.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.3.tgz",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"@capacitor/network": "^8.0.0",
|
"@capacitor/network": "^8.0.0",
|
||||||
"@capacitor/preferences": "^8.0.0",
|
"@capacitor/preferences": "^8.0.0",
|
||||||
"@capacitor/push-notifications": "^8.0.0",
|
"@capacitor/push-notifications": "^8.0.0",
|
||||||
|
"@capacitor/share": "^8.0.1",
|
||||||
"@capacitor/status-bar": "^8.0.0",
|
"@capacitor/status-bar": "^8.0.0",
|
||||||
"@capgo/capacitor-document-scanner": "^8.4.0",
|
"@capgo/capacitor-document-scanner": "^8.4.0",
|
||||||
"@capgo/capacitor-nfc": "^8.0.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*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
12
public/.well-known/assetlinks.json
Normal file
12
public/.well-known/assetlinks.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
5
public/_headers
Normal file
5
public/_headers
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/.well-known/apple-app-site-association
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
/.well-known/assetlinks.json
|
||||||
|
Content-Type: application/json
|
||||||
@@ -6,6 +6,7 @@ import { LocalNotifications } from '@capacitor/local-notifications'
|
|||||||
import { Preferences } from '@capacitor/preferences'
|
import { Preferences } from '@capacitor/preferences'
|
||||||
import { PushNotifications } from '@capacitor/push-notifications'
|
import { PushNotifications } from '@capacitor/push-notifications'
|
||||||
import { focusManager } from '@tanstack/react-query'
|
import { focusManager } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { RegisterDeviceToken } from './utils/Fetcher'
|
import { RegisterDeviceToken } from './utils/Fetcher'
|
||||||
import { beginOAuthExchange } from './utils/OAuthExchangeState'
|
import { beginOAuthExchange } from './utils/OAuthExchangeState'
|
||||||
|
|
||||||
@@ -63,7 +64,22 @@ const handleNFCChoreDeepLink = (url, isColdStart) => {
|
|||||||
|
|
||||||
const handleUrlOpen = (url, isColdStart = false) => {
|
const handleUrlOpen = (url, isColdStart = false) => {
|
||||||
console.log('[NFC] handleUrlOpen:', url)
|
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) {
|
||||||
|
routerNavigate(`/circle/join${parsedUrl.search}`)
|
||||||
|
} else if (url.startsWith('donetick://chores/add')) {
|
||||||
// Widget "+" / quick-capture buttons: land on the chore list with the
|
// Widget "+" / quick-capture buttons: land on the chore list with the
|
||||||
// quick-add modal open (MyChores watches for the add_task param and
|
// quick-add modal open (MyChores watches for the add_task param and
|
||||||
// consumes it). ?mode=scan|voice opens straight into that capture panel.
|
// consumes it). ?mode=scan|voice opens straight into that capture panel.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Delete, Refresh } from '@mui/icons-material'
|
import { Share } from '@capacitor/share'
|
||||||
|
import { Delete, IosShare, Refresh } from '@mui/icons-material'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -9,15 +10,17 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Option,
|
Option,
|
||||||
Select,
|
Select,
|
||||||
Typography
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||||
import { useUserProfile } from '../../queries/UserQueries'
|
import { useUserProfile } from '../../queries/UserQueries'
|
||||||
import { useNotification } from '../../service/NotificationProvider'
|
import { useNotification } from '../../service/NotificationProvider'
|
||||||
|
import { apiClient } from '../../utils/ApiClient'
|
||||||
import {
|
import {
|
||||||
AcceptCircleMemberRequest,
|
AcceptCircleMemberRequest,
|
||||||
DeleteCircleMember,
|
DeleteCircleMember,
|
||||||
@@ -115,6 +118,36 @@ const CircleSettings = () => {
|
|||||||
}
|
}
|
||||||
}, [circleMembers, userProfile])
|
}, [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) {
|
if (!userProfile) {
|
||||||
return <LoadingComponent />
|
return <LoadingComponent />
|
||||||
}
|
}
|
||||||
@@ -134,7 +167,7 @@ const CircleSettings = () => {
|
|||||||
: `You circle code is:`}
|
: `You circle code is:`}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
value={userCircles[0]?.invite_code}
|
value={inviteCode}
|
||||||
disabled
|
disabled
|
||||||
size='lg'
|
size='lg'
|
||||||
sx={{
|
sx={{
|
||||||
@@ -156,14 +189,19 @@ const CircleSettings = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='soft'
|
variant='soft'
|
||||||
|
disabled={!inviteLink}
|
||||||
|
startDecorator={<IosShare />}
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
onClick={shareInvite}
|
||||||
|
>
|
||||||
|
Share Invite
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='soft'
|
||||||
|
disabled={!inviteLink}
|
||||||
sx={{ ml: 1 }}
|
sx={{ ml: 1 }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(inviteLink)
|
||||||
window.location.protocol +
|
|
||||||
'//' +
|
|
||||||
window.location.host +
|
|
||||||
`/circle/join?code=${userCircles[0]?.invite_code}`,
|
|
||||||
)
|
|
||||||
showNotification({
|
showNotification({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: 'Link copied to clipboard',
|
message: 'Link copied to clipboard',
|
||||||
@@ -227,8 +265,7 @@ const CircleSettings = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography level='body-sm' color='danger'>
|
<Typography level='body-sm' color='danger'>
|
||||||
Request to join{' '}
|
Request to join {fmt.date(member.updatedAt)}
|
||||||
{fmt.date(member.updatedAt)}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user