+5
-5
src/Splash.web.tsx
+5
-5
src/Splash.web.tsx
···
9
9
10
10
import {atoms as a} from '#/alf'
11
11
12
-
const size = 100
13
-
const ratio = 57 / 64
12
+
const size = 125
13
+
const ratio = 512 / 512
14
14
15
15
export function Splash() {
16
16
return (
17
17
<View style={[a.fixed, a.inset_0, a.align_center, a.justify_center]}>
18
18
<Svg
19
19
fill="none"
20
-
viewBox="0 0 64 57"
20
+
viewBox="0 0 512 512"
21
21
style={[a.relative, {width: size, height: size * ratio, top: -50}]}>
22
22
<Path
23
-
fill="#006AFF"
24
-
d="M13.873 3.805C21.21 9.332 29.103 20.537 32 26.55v15.882c0-.338-.13.044-.41.867-1.512 4.456-7.418 21.847-20.923 7.944-7.111-7.32-3.819-14.64 9.125-16.85-7.405 1.264-15.73-.825-18.014-9.015C1.12 23.022 0 8.51 0 6.55 0-3.268 8.579-.182 13.873 3.805ZM50.127 3.805C42.79 9.332 34.897 20.537 32 26.55v15.882c0-.338.13.044.41.867 1.512 4.456 7.418 21.847 20.923 7.944 7.111-7.32 3.819-14.64-9.125-16.85 7.405 1.264 15.73-.825 18.014-9.015C62.88 23.022 64 8.51 64 6.55c0-9.818-8.578-6.732-13.873-2.745Z"
23
+
fill="#ED5345"
24
+
d="M374.473 57.7173C367.666 50.7995 357.119 49.1209 348.441 53.1659C347.173 53.7567 342.223 56.0864 334.796 59.8613C326.32 64.1696 314.568 70.3869 301.394 78.0596C275.444 93.1728 242.399 114.83 218.408 139.477C185.983 172.786 158.719 225.503 140.029 267.661C130.506 289.144 122.878 308.661 117.629 322.81C116.301 326.389 115.124 329.63 114.104 332.478C87.1783 336.42 64.534 341.641 47.5078 348.101C37.6493 351.84 28.3222 356.491 21.0573 362.538C13.8818 368.511 6.00003 378.262 6.00003 391.822C6.00014 403.222 11.8738 411.777 17.4566 417.235C23.0009 422.655 29.9593 426.793 36.871 430.062C50.8097 436.653 69.5275 441.988 90.8362 446.249C133.828 454.846 192.21 460 256.001 460C319.79 460 378.172 454.846 421.164 446.249C442.472 441.988 461.19 436.653 475.129 430.062C482.041 426.793 488.999 422.655 494.543 417.235C500.039 411.862 505.817 403.489 505.996 392.353L506 391.822L505.995 391.188C505.754 377.959 498.012 368.417 490.945 362.534C483.679 356.485 474.35 351.835 464.491 348.095C446.749 341.366 422.906 335.982 394.476 331.987C393.6 330.57 392.633 328.995 391.595 327.273C386.477 318.777 379.633 306.842 372.737 293.115C358.503 264.781 345.757 232.098 344.756 206.636C343.87 184.121 351.638 154.087 360.819 127.789C365.27 115.041 369.795 103.877 373.207 95.9072C374.909 91.9309 376.325 88.7712 377.302 86.6328C377.79 85.5645 378.167 84.7524 378.416 84.2224C378.54 83.9579 378.632 83.7635 378.69 83.643C378.718 83.5829 378.739 83.5411 378.75 83.5181C378.753 83.5108 378.756 83.5049 378.757 83.5015C382.909 74.8634 381.196 64.5488 374.473 57.7173Z"
25
25
/>
26
26
</Svg>
27
27
</View>
+57
-94
src/components/dialogs/ServerInput.tsx
+57
-94
src/components/dialogs/ServerInput.tsx
···
12
12
import {Admonition} from '#/components/Admonition'
13
13
import {Button, ButtonText} from '#/components/Button'
14
14
import * as Dialog from '#/components/Dialog'
15
-
import * as SegmentedControl from '#/components/forms/SegmentedControl'
16
15
import * as TextField from '#/components/forms/TextField'
17
16
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
18
17
import {InlineLinkText} from '#/components/Link'
19
18
import {Text} from '#/components/Typography'
20
-
21
-
type SegmentedControlOptions = typeof BSKY_SERVICE | 'custom'
22
19
23
20
export function ServerInputDialog({
24
21
control,
···
31
28
const formRef = useRef<DialogInnerRef>(null)
32
29
33
30
// persist these options between dialog open/close
34
-
const [fixedOption, setFixedOption] =
35
-
useState<SegmentedControlOptions>(BSKY_SERVICE)
36
31
const [previousCustomAddress, setPreviousCustomAddress] = useState('')
37
32
38
33
const onClose = useCallback(() => {
···
44
39
}
45
40
}
46
41
logger.metric('signin:hostingProviderPressed', {
47
-
hostingProviderDidChange: fixedOption !== BSKY_SERVICE,
42
+
hostingProviderDidChange: false, // stubbed for PDS auto-resolution
48
43
})
49
-
}, [onSelect, fixedOption])
44
+
}, [onSelect])
50
45
51
46
return (
52
47
<Dialog.Outer
···
59
54
<Dialog.Handle />
60
55
<DialogInner
61
56
formRef={formRef}
62
-
fixedOption={fixedOption}
63
-
setFixedOption={setFixedOption}
64
57
initialCustomAddress={previousCustomAddress}
65
58
/>
66
59
</Dialog.Outer>
···
71
64
72
65
function DialogInner({
73
66
formRef,
74
-
fixedOption,
75
-
setFixedOption,
76
67
initialCustomAddress,
77
68
}: {
78
69
formRef: React.Ref<DialogInnerRef>
79
-
fixedOption: SegmentedControlOptions
80
-
setFixedOption: (opt: SegmentedControlOptions) => void
81
70
initialCustomAddress: string
82
71
}) {
83
72
const control = Dialog.useDialogContext()
···
94
83
formRef,
95
84
() => ({
96
85
getFormState: () => {
97
-
let url
98
-
if (fixedOption === 'custom') {
99
-
url = customAddress.trim().toLowerCase()
100
-
if (!url) {
101
-
return null
102
-
}
103
-
} else {
104
-
url = fixedOption
86
+
let url = customAddress.trim().toLowerCase()
87
+
if (!url) {
88
+
return null
105
89
}
106
90
if (!url.startsWith('http://') && !url.startsWith('https://')) {
107
91
if (url === 'localhost' || url.startsWith('localhost:')) {
···
111
95
}
112
96
}
113
97
114
-
if (fixedOption === 'custom') {
115
-
if (!pdsAddressHistory.includes(url)) {
116
-
const newHistory = [url, ...pdsAddressHistory.slice(0, 4)]
117
-
setPdsAddressHistory(newHistory)
118
-
persisted.write('pdsAddressHistory', newHistory)
119
-
}
98
+
if (!pdsAddressHistory.includes(url)) {
99
+
const newHistory = [url, ...pdsAddressHistory.slice(0, 4)]
100
+
setPdsAddressHistory(newHistory)
101
+
persisted.write('pdsAddressHistory', newHistory)
120
102
}
121
103
122
104
return url
123
105
},
124
106
}),
125
-
[customAddress, fixedOption, pdsAddressHistory],
107
+
[customAddress, pdsAddressHistory],
126
108
)
127
109
128
110
const isFirstTimeUser = accounts.length === 0
···
136
118
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
137
119
<Trans>Choose your account provider</Trans>
138
120
</Text>
139
-
<SegmentedControl.Root
140
-
type="tabs"
141
-
label={_(msg`Account provider`)}
142
-
value={fixedOption}
143
-
onChange={setFixedOption}>
144
-
<SegmentedControl.Item
145
-
testID="bskyServiceSelectBtn"
146
-
value={BSKY_SERVICE}
147
-
label={_(msg`Bluesky`)}>
148
-
<SegmentedControl.ItemText>
149
-
{_(msg`Bluesky`)}
150
-
</SegmentedControl.ItemText>
151
-
</SegmentedControl.Item>
152
-
<SegmentedControl.Item
153
-
testID="customSelectBtn"
154
-
value="custom"
155
-
label={_(msg`Custom`)}>
156
-
<SegmentedControl.ItemText>
157
-
{_(msg`Custom`)}
158
-
</SegmentedControl.ItemText>
159
-
</SegmentedControl.Item>
160
-
</SegmentedControl.Root>
161
121
162
-
{fixedOption === BSKY_SERVICE && isFirstTimeUser && (
163
-
<View role="tabpanel">
164
-
<Admonition type="tip">
165
-
<Trans>
166
-
Bluesky is an open network where you can choose your own
167
-
provider. If you're new here, we recommend sticking with the
168
-
default Bluesky Social option.
169
-
</Trans>
170
-
</Admonition>
171
-
</View>
122
+
{isFirstTimeUser && (
123
+
<Admonition type="tip">
124
+
<Trans>
125
+
Bluesky is an open network where you can choose your own provider.
126
+
If you're new here, we recommend sticking with the default Bluesky
127
+
Social option.
128
+
</Trans>
129
+
</Admonition>
172
130
)}
173
131
174
-
{fixedOption === 'custom' && (
175
-
<View role="tabpanel">
176
-
<TextField.LabelText nativeID="address-input-label">
177
-
<Trans>Server address</Trans>
178
-
</TextField.LabelText>
179
-
<TextField.Root>
180
-
<TextField.Icon icon={Globe} />
181
-
<Dialog.Input
182
-
testID="customServerTextInput"
183
-
value={customAddress}
184
-
onChangeText={setCustomAddress}
185
-
label="my-server.com"
186
-
accessibilityLabelledBy="address-input-label"
187
-
autoCapitalize="none"
188
-
keyboardType="url"
189
-
/>
190
-
</TextField.Root>
191
-
{pdsAddressHistory.length > 0 && (
192
-
<View style={[a.flex_row, a.flex_wrap, a.mt_xs]}>
193
-
{pdsAddressHistory.map(uri => (
194
-
<Button
195
-
key={uri}
196
-
variant="ghost"
197
-
color="primary"
198
-
label={uri}
199
-
style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]}
200
-
onPress={() => setCustomAddress(uri)}>
201
-
<ButtonText>{uri}</ButtonText>
202
-
</Button>
203
-
))}
204
-
</View>
205
-
)}
206
-
</View>
207
-
)}
132
+
<View
133
+
style={[
134
+
a.border,
135
+
t.atoms.border_contrast_low,
136
+
a.rounded_sm,
137
+
a.px_md,
138
+
a.py_md,
139
+
]}>
140
+
<TextField.LabelText nativeID="address-input-label">
141
+
<Trans>Server address</Trans>
142
+
</TextField.LabelText>
143
+
<TextField.Root>
144
+
<TextField.Icon icon={Globe} />
145
+
<Dialog.Input
146
+
testID="customServerTextInput"
147
+
value={customAddress}
148
+
onChangeText={setCustomAddress}
149
+
label="my-server.com"
150
+
accessibilityLabelledBy="address-input-label"
151
+
autoCapitalize="none"
152
+
keyboardType="url"
153
+
/>
154
+
</TextField.Root>
155
+
{pdsAddressHistory.length > 0 && (
156
+
<View style={[a.flex_row, a.flex_wrap, a.mt_xs]}>
157
+
{pdsAddressHistory.map(uri => (
158
+
<Button
159
+
key={uri}
160
+
variant="ghost"
161
+
color="primary"
162
+
label={uri}
163
+
style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]}
164
+
onPress={() => setCustomAddress(uri)}>
165
+
<ButtonText>{uri}</ButtonText>
166
+
</Button>
167
+
))}
168
+
</View>
169
+
)}
170
+
</View>
208
171
209
172
<View style={[a.py_xs]}>
210
173
<Text
+7
-7
src/components/forms/HostingProvider.tsx
+7
-7
src/components/forms/HostingProvider.tsx
···
18
18
onOpenDialog,
19
19
minimal,
20
20
}: {
21
-
serviceUrl: string
21
+
serviceUrl?: string | undefined
22
22
onSelectServiceUrl: (provider: string) => void
23
23
onOpenDialog?: () => void
24
24
minimal?: boolean
···
26
26
const serverInputControl = useDialogControl()
27
27
const t = useTheme()
28
28
const {_} = useLingui()
29
+
const serviceProviderLabel =
30
+
serviceUrl === undefined ? _(msg`Automatic`) : toNiceDomain(serviceUrl)
29
31
30
32
const onPressSelectService = React.useCallback(() => {
31
33
Keyboard.dismiss()
···
45
47
<Trans>You are creating an account on</Trans>
46
48
</Text>
47
49
<Button
48
-
label={toNiceDomain(serviceUrl)}
50
+
label={serviceProviderLabel}
49
51
accessibilityHint={_(msg`Changes hosting provider`)}
50
52
onPress={onPressSelectService}
51
53
variant="ghost"
···
56
58
{marginHorizontal: tokens.space.xs * -1},
57
59
{paddingVertical: 0},
58
60
]}>
59
-
<ButtonText style={[a.text_sm]}>
60
-
{toNiceDomain(serviceUrl)}
61
-
</ButtonText>
61
+
<ButtonText style={[a.text_sm]}>{serviceProviderLabel}</ButtonText>
62
62
<ButtonIcon icon={PencilIcon} />
63
63
</Button>
64
64
</View>
65
65
) : (
66
66
<Button
67
67
testID="selectServiceButton"
68
-
label={toNiceDomain(serviceUrl)}
68
+
label={serviceProviderLabel}
69
69
accessibilityHint={_(msg`Changes hosting provider`)}
70
70
variant="solid"
71
71
color="secondary"
···
94
94
}
95
95
/>
96
96
</View>
97
-
<Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text>
97
+
<Text style={[a.text_md]}>{serviceProviderLabel}</Text>
98
98
<View
99
99
style={[
100
100
a.rounded_sm,
+27
-2
src/screens/Login/LoginForm.tsx
+27
-2
src/screens/Login/LoginForm.tsx
···
48
48
onPressForgotPassword,
49
49
onAttemptSuccess,
50
50
onAttemptFailed,
51
+
debouncedResolveService,
52
+
isResolvingService,
51
53
}: {
52
54
error: string
53
-
serviceUrl: string
55
+
serviceUrl?: string | undefined
54
56
serviceDescription: ServiceDescription | undefined
55
57
initialHandle: string
56
58
setError: (v: string) => void
···
60
62
onPressForgotPassword: () => void
61
63
onAttemptSuccess: () => void
62
64
onAttemptFailed: () => void
65
+
debouncedResolveService: (identifier: string) => void
66
+
isResolvingService: boolean
63
67
}) => {
64
68
const t = useTheme()
65
69
const [isProcessing, setIsProcessing] = useState<boolean>(false)
···
97
101
98
102
if (!password) {
99
103
setError(_(msg`Please enter your password`))
104
+
return
105
+
}
106
+
107
+
if (!serviceUrl) {
108
+
setError(_(msg`Please enter hosting provider URL`))
100
109
return
101
110
}
102
111
···
182
191
<View>
183
192
<TextField.LabelText>
184
193
<Trans>Hosting provider</Trans>
194
+
{isResolvingService && (
195
+
<ActivityIndicator
196
+
size={12}
197
+
color={t.palette.contrast_500}
198
+
style={a.ml_sm}
199
+
/>
200
+
)}
185
201
</TextField.LabelText>
186
202
<HostingProvider
187
203
serviceUrl={serviceUrl}
···
209
225
defaultValue={initialHandle || ''}
210
226
onChangeText={v => {
211
227
identifierValueRef.current = v
228
+
// Trigger PDS auto-resolution for handles/DIDs
229
+
const id = v.trim()
230
+
if (!id) return
231
+
if (
232
+
id.startsWith('did:') ||
233
+
(id.includes('.') && !id.includes('@'))
234
+
) {
235
+
debouncedResolveService(id)
236
+
}
212
237
}}
213
238
onSubmitEditing={() => {
214
239
passwordRef.current?.focus()
···
333
358
<Trans>Retry</Trans>
334
359
</ButtonText>
335
360
</Button>
336
-
) : !serviceDescription ? (
361
+
) : !serviceDescription && serviceUrl !== undefined ? (
337
362
<>
338
363
<ActivityIndicator color={t.palette.primary_500} />
339
364
<Text style={[t.atoms.text_contrast_high, a.pl_md]}>
+56
-8
src/screens/Login/index.tsx
+56
-8
src/screens/Login/index.tsx
···
1
-
import {useEffect, useRef, useState} from 'react'
1
+
import {useCallback,useEffect, useMemo, useRef, useState} from 'react'
2
2
import {KeyboardAvoidingView} from 'react-native'
3
3
import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated'
4
+
import {type Did} from '@atproto/api'
4
5
import {msg} from '@lingui/macro'
5
6
import {useLingui} from '@lingui/react'
7
+
import debounce from 'lodash.debounce'
6
8
7
9
import {DEFAULT_SERVICE} from '#/lib/constants'
8
10
import {logEvent} from '#/lib/statsig/statsig'
9
11
import {logger} from '#/logger'
12
+
import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity'
10
13
import {useServiceQuery} from '#/state/queries/service'
11
-
import {type SessionAccount, useSession} from '#/state/session'
14
+
import {type SessionAccount, useAgent, useSession} from '#/state/session'
12
15
import {useLoggedOutView} from '#/state/shell/logged-out'
13
16
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
14
17
import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm'
···
40
43
const failedAttemptCountRef = useRef(0)
41
44
const startTimeRef = useRef(Date.now())
42
45
46
+
const agent = useAgent()
43
47
const {accounts} = useSession()
44
48
const {requestedAccountSwitchTo} = useLoggedOutView()
45
49
const requestedAccount = accounts.find(
46
50
acc => acc.did === requestedAccountSwitchTo,
47
51
)
48
52
49
-
const [error, setError] = useState('')
50
-
const [serviceUrl, setServiceUrl] = useState(
51
-
requestedAccount?.service || DEFAULT_SERVICE,
53
+
const [isResolvingService, setIsResolvingService] = useState(false)
54
+
const [error, setError] = useState<string>('')
55
+
const [serviceUrl, setServiceUrl] = useState<string | undefined>(
56
+
requestedAccount?.service,
52
57
)
53
58
const [initialHandle, setInitialHandle] = useState(
54
59
requestedAccount?.handle || '',
···
68
73
data: serviceDescription,
69
74
error: serviceError,
70
75
refetch: refetchService,
71
-
} = useServiceQuery(serviceUrl)
76
+
} = useServiceQuery(serviceUrl ?? '')
72
77
73
78
const onSelectAccount = (account?: SessionAccount) => {
74
79
if (account?.service) {
···
102
107
}
103
108
}, [serviceError, serviceUrl, _])
104
109
110
+
const resolveIdentity = useCallback(
111
+
async (identifier: string) => {
112
+
setIsResolvingService(true)
113
+
114
+
try {
115
+
const getDid = async () => {
116
+
if (identifier.startsWith('did:')) return identifier
117
+
else
118
+
return (
119
+
await agent.resolveHandle({
120
+
handle: identifier,
121
+
})
122
+
).data.did
123
+
}
124
+
125
+
const did = (await getDid()) as Did
126
+
const pdsUrl = await resolvePdsServiceUrl(did)
127
+
128
+
if (!pdsUrl) {
129
+
throw new Error(`No PDS service found in DID document for ${did}`)
130
+
}
131
+
132
+
if (pdsUrl.endsWith('.bsky.network')) {
133
+
setServiceUrl('https://bsky.social')
134
+
} else {
135
+
setServiceUrl(pdsUrl)
136
+
}
137
+
} catch (err) {
138
+
logger.error(`Service auto-resolution failed: ${err}`)
139
+
} finally {
140
+
setIsResolvingService(false)
141
+
}
142
+
},
143
+
[agent],
144
+
)
145
+
146
+
const debouncedResolveService = useMemo(
147
+
() => debounce(resolveIdentity, 800),
148
+
[resolveIdentity],
149
+
)
150
+
105
151
const onPressForgotPassword = () => {
106
152
gotoForm(Forms.ForgotPassword)
107
153
logEvent('signin:forgotPasswordPressed', {})
···
150
196
}
151
197
onPressForgotPassword={onPressForgotPassword}
152
198
onPressRetryConnect={refetchService}
199
+
debouncedResolveService={debouncedResolveService}
200
+
isResolvingService={isResolvingService}
153
201
/>
154
202
)
155
203
break
···
169
217
content = (
170
218
<ForgotPasswordForm
171
219
error={error}
172
-
serviceUrl={serviceUrl}
220
+
serviceUrl={serviceUrl ?? DEFAULT_SERVICE}
173
221
serviceDescription={serviceDescription}
174
222
setError={setError}
175
223
setServiceUrl={setServiceUrl}
···
184
232
content = (
185
233
<SetNewPasswordForm
186
234
error={error}
187
-
serviceUrl={serviceUrl}
235
+
serviceUrl={serviceUrl ?? DEFAULT_SERVICE}
188
236
setError={setError}
189
237
onPressBack={() => gotoForm(Forms.ForgotPassword)}
190
238
onPasswordSet={() => gotoForm(Forms.PasswordUpdated)}
+13
-1
src/screens/Settings/DeerSettings.tsx
+13
-1
src/screens/Settings/DeerSettings.tsx
···
460
460
const showLinkInHandle = useShowLinkInHandle()
461
461
const setShowLinkInHandle = useSetShowLinkInHandle()
462
462
463
-
const [gates, setGatesView] = useState(Object.fromEntries(useGatesCache()))
463
+
const [gates, setGatesView] = useState(Object.assign({
464
+
alt_share_icon: false,
465
+
debug_show_feedcontext: false,
466
+
debug_subscriptions: false,
467
+
explore_show_suggested_feeds: false,
468
+
feed_reply_button_open_thread: false,
469
+
old_postonboarding: false,
470
+
onboarding_add_video_feed: false,
471
+
onboarding_suggested_starterpacks: false,
472
+
remove_show_latest_button: false,
473
+
test_gate_1: false,
474
+
test_gate_2: false,
475
+
} satisfies Record<Gate, false>, Object.fromEntries(useGatesCache())))
464
476
const dangerousSetGate = useDangerousSetGate()
465
477
const setGate = (gate: Gate, value: boolean) => {
466
478
dangerousSetGate(gate, value)