get contacts

This commit is contained in:
Samuel Newman
2025-11-28 19:32:31 +02:00
parent feddd552e7
commit a49c035d07
3 changed files with 114 additions and 18 deletions
+101 -8
View File
@@ -1,15 +1,21 @@
import {View} from 'react-native'
import {Alert, View} from 'react-native'
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 {useMutation} from '@tanstack/react-query'
import {wait} from '#/lib/async/wait'
import {logger} from '#/logger'
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 {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {type Action, type State} from '../state'
export function GetContacts({
dispatch,
onCancel,
}: {
state: Extract<State, {step: '3: get contacts'}>
@@ -18,9 +24,63 @@ export function GetContacts({
}) {
const {_} = useLingui()
const insets = useSafeAreaInsets()
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]
return (
@@ -70,10 +130,24 @@ export function GetContacts({
and to retain hashed data for matching until I opt out.
</Trans>
</Text>
<Button label={_(msg`Find my friends`)} size="large" color="primary">
<ButtonText>
<Trans>Find my friends</Trans>
</ButtonText>
<Button
label={_(msg`Find my friends`)}
size="large"
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
label={_(msg`Cancel`)}
@@ -88,3 +162,22 @@ export function GetContacts({
</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 () => {
await wait(2e3, () => {})
dispatch({type: 'VERIFY_PHONE_NUMBER_SUCCESS'})
dispatch({
type: 'VERIFY_PHONE_NUMBER_SUCCESS',
payload: {
token: 'example_token',
},
})
},
onMutate: () => setError(null),
onError: err => {
+7 -9
View File
@@ -1,16 +1,9 @@
import {useReducer} from 'react'
import {type ExistingContact} from 'expo-contacts'
import type * as bsky from '#/types/bsky'
export type Contact = {
/**
* Generate ourselves - should be random or sequential
*/
id: string
firstName?: string
lastName?: string
phone?: string[]
}
export type Contact = ExistingContact
// TODO: replace with lexicon type
export type Match = {
@@ -32,6 +25,7 @@ export type State =
}
| {
step: '3: get contacts'
token: string
contacts?: Contact[]
}
| {
@@ -53,6 +47,9 @@ export type Action =
}
| {
type: 'VERIFY_PHONE_NUMBER_SUCCESS'
payload: {
token: string
}
}
| {
type: 'GET_CONTACTS_SUCCESS'
@@ -91,6 +88,7 @@ function reducer(state: State, action: Action): State {
assertCurrentStep(state, '2: verify number')
return {
step: '3: get contacts',
token: action.payload.token,
}
}
case 'BACK': {