get contacts
This commit is contained in:
@@ -1,15 +1,21 @@
|
|||||||
import {View} from 'react-native'
|
import {Alert, View} from 'react-native'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import * as Contacts from 'expo-contacts'
|
||||||
|
import {msg, t, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {wait} from '#/lib/async/wait'
|
||||||
|
import {logger} from '#/logger'
|
||||||
import {atoms as a, tokens, useGutters} from '#/alf'
|
import {atoms as a, tokens, useGutters} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {type Action, type State} from '../state'
|
import {type Action, type State} from '../state'
|
||||||
|
|
||||||
export function GetContacts({
|
export function GetContacts({
|
||||||
|
dispatch,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: {
|
}: {
|
||||||
state: Extract<State, {step: '3: get contacts'}>
|
state: Extract<State, {step: '3: get contacts'}>
|
||||||
@@ -18,9 +24,63 @@ export function GetContacts({
|
|||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
const gutters = useGutters([0, 'wide'])
|
const gutters = useGutters([0, 'wide'])
|
||||||
|
|
||||||
|
const {mutate: uploadContacts, isPending: isUploadPending} = useMutation({
|
||||||
|
mutationFn: async (_contacts: Contacts.ExistingContact[]) => {
|
||||||
|
await wait(2e3, () => {})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SYNC_CONTACTS_SUCCESS',
|
||||||
|
payload: {
|
||||||
|
matches: [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const {mutate: getContacts, isPending: isGetContactsPending} = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
let permissions = await Contacts.getPermissionsAsync()
|
||||||
|
|
||||||
|
if (!permissions.granted && permissions.canAskAgain) {
|
||||||
|
permissions = await Contacts.requestPermissionsAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!permissions.granted) {
|
||||||
|
throw new PermissionDeniedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const contacts = await Contacts.getContactsAsync({
|
||||||
|
fields: [
|
||||||
|
Contacts.Fields.FirstName,
|
||||||
|
Contacts.Fields.LastName,
|
||||||
|
Contacts.Fields.PhoneNumbers,
|
||||||
|
Contacts.Fields.Image,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return contacts.data
|
||||||
|
},
|
||||||
|
onSuccess: contacts => {
|
||||||
|
dispatch({
|
||||||
|
type: 'GET_CONTACTS_SUCCESS',
|
||||||
|
payload: {contacts},
|
||||||
|
})
|
||||||
|
uploadContacts(contacts)
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
if (err instanceof PermissionDeniedError) {
|
||||||
|
showPermissionDeniedAlert()
|
||||||
|
} else {
|
||||||
|
logger.error('Error getting contacts', {safeMessage: err})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const isPending = isUploadPending || isGetContactsPending
|
||||||
|
|
||||||
const style = [a.text_md, a.leading_snug, a.mt_md]
|
const style = [a.text_md, a.leading_snug, a.mt_md]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -70,10 +130,24 @@ export function GetContacts({
|
|||||||
and to retain hashed data for matching until I opt out.
|
and to retain hashed data for matching until I opt out.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Button label={_(msg`Find my friends`)} size="large" color="primary">
|
<Button
|
||||||
<ButtonText>
|
label={_(msg`Find my friends`)}
|
||||||
<Trans>Find my friends</Trans>
|
size="large"
|
||||||
</ButtonText>
|
color="primary"
|
||||||
|
onPress={() => getContacts()}
|
||||||
|
disabled={isPending}>
|
||||||
|
{isUploadPending ? (
|
||||||
|
<>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Finding friends...</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={Loader} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Find my friends</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Cancel`)}
|
label={_(msg`Cancel`)}
|
||||||
@@ -88,3 +162,22 @@ export function GetContacts({
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PermissionDeniedError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Permission denied')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPermissionDeniedAlert() {
|
||||||
|
Alert.alert(
|
||||||
|
t`You've denied access to your contacts`,
|
||||||
|
t`You'll need to go to the app's settings and give permission if you want to use this feature.`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: t`Ok`,
|
||||||
|
style: 'default',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,7 +60,12 @@ export function VerifyNumber({
|
|||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await wait(2e3, () => {})
|
await wait(2e3, () => {})
|
||||||
dispatch({type: 'VERIFY_PHONE_NUMBER_SUCCESS'})
|
dispatch({
|
||||||
|
type: 'VERIFY_PHONE_NUMBER_SUCCESS',
|
||||||
|
payload: {
|
||||||
|
token: 'example_token',
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onMutate: () => setError(null),
|
onMutate: () => setError(null),
|
||||||
onError: err => {
|
onError: err => {
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import {useReducer} from 'react'
|
import {useReducer} from 'react'
|
||||||
|
import {type ExistingContact} from 'expo-contacts'
|
||||||
|
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
export type Contact = {
|
export type Contact = ExistingContact
|
||||||
/**
|
|
||||||
* Generate ourselves - should be random or sequential
|
|
||||||
*/
|
|
||||||
id: string
|
|
||||||
firstName?: string
|
|
||||||
lastName?: string
|
|
||||||
phone?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: replace with lexicon type
|
// TODO: replace with lexicon type
|
||||||
export type Match = {
|
export type Match = {
|
||||||
@@ -32,6 +25,7 @@ export type State =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
step: '3: get contacts'
|
step: '3: get contacts'
|
||||||
|
token: string
|
||||||
contacts?: Contact[]
|
contacts?: Contact[]
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -53,6 +47,9 @@ export type Action =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'VERIFY_PHONE_NUMBER_SUCCESS'
|
type: 'VERIFY_PHONE_NUMBER_SUCCESS'
|
||||||
|
payload: {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'GET_CONTACTS_SUCCESS'
|
type: 'GET_CONTACTS_SUCCESS'
|
||||||
@@ -91,6 +88,7 @@ function reducer(state: State, action: Action): State {
|
|||||||
assertCurrentStep(state, '2: verify number')
|
assertCurrentStep(state, '2: verify number')
|
||||||
return {
|
return {
|
||||||
step: '3: get contacts',
|
step: '3: get contacts',
|
||||||
|
token: action.payload.token,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'BACK': {
|
case 'BACK': {
|
||||||
|
|||||||
Reference in New Issue
Block a user