)[key] === undefined
+ ) {
+ throw new Error(`Missing claim: ${key}`)
+ }
+ }
+ }
+
+ console.log(payload)
+
+ if (payload.iat == null) {
+ throw new Error('Missing issued at')
+ }
+
+ const now = (options?.currentDate?.getTime() ?? Date.now()) / 1e3
+ const clockTolerance = options?.clockTolerance ?? 0
+
+ if (options?.maxTokenAge != null) {
+ if (payload.iat < now - options.maxTokenAge + clockTolerance) {
+ throw new Error('Invalid issued at')
+ }
+ }
+
+ if (payload.nbf != null) {
+ if (payload.nbf > now - clockTolerance) {
+ throw new Error('Invalid not before')
+ }
+ }
+
+ if (payload.exp != null) {
+ if (payload.exp < now + clockTolerance) {
+ throw new Error('Invalid expiration')
+ }
+ }
+
+ return {payload, protectedHeader} as VerifyResult
+ }
+}
diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts
new file mode 100644
index 0000000000..ed6a292c79
--- /dev/null
+++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native.ts
@@ -0,0 +1,149 @@
+import {Fetch} from '@atproto/fetch'
+import {UniversalIdentityResolver} from '@atproto/identity-resolver'
+import {
+ OAuthAuthorizeOptions,
+ OAuthClientFactory,
+ OAuthResponseMode,
+ OAuthResponseType,
+ Session,
+} from '@atproto/oauth-client'
+import {OAuthClientMetadata} from '@atproto/oauth-client-metadata'
+import {IsomorphicOAuthServerMetadataResolver} from '@atproto/oauth-server-metadata-resolver'
+
+import {ReactNativeCryptoImplementation} from './react-native-crypto-implementation'
+import {DatabaseStore} from './react-native-oauth-database'
+import {RNOAuthDatabase} from './react-native-oauth-database.native'
+
+export type RNOAuthClientOptions = {
+ responseMode?: OAuthResponseMode
+ responseType?: OAuthResponseType
+ clientMetadata: OAuthClientMetadata
+ fetch?: Fetch
+ crypto?: any
+ plcDirectoryUrl?: string
+ atprotoLexiconUrl?: string
+}
+
+export class RNOAuthClientFactory extends OAuthClientFactory {
+ readonly sessionStore: DatabaseStore
+
+ constructor({
+ clientMetadata,
+ // "fragment" is safer as it is not sent to the server
+ responseMode = 'fragment',
+ fetch = globalThis.fetch,
+ plcDirectoryUrl,
+ atprotoLexiconUrl,
+ }: RNOAuthClientOptions) {
+ const database = new RNOAuthDatabase()
+
+ super({
+ clientMetadata,
+ responseMode,
+ fetch,
+ cryptoImplementation: new ReactNativeCryptoImplementation(),
+ sessionStore: database.getSessionStore(),
+ stateStore: database.getStateStore(),
+ metadataResolver: new IsomorphicOAuthServerMetadataResolver({
+ fetch,
+ cache: database.getMetadataCache(),
+ }),
+ identityResolver: UniversalIdentityResolver.from({
+ fetch,
+ plcDirectoryUrl,
+ atprotoLexiconUrl,
+ didCache: database.getDidCache(),
+ handleCache: database.getHandleCache(),
+ }),
+ dpopNonceCache: database.getDpopNonceCache(),
+ })
+
+ this.sessionStore = database.getSessionStore()
+ }
+
+ async restoreAll() {
+ const sessionIds = await this.sessionStore.getKeys()
+ return Object.fromEntries(
+ await Promise.all(
+ sessionIds.map(
+ async sessionId =>
+ [sessionId, await this.restore(sessionId, false)] as const,
+ ),
+ ),
+ )
+ }
+
+ // async init(sessionId?: string, forceRefresh = false) {
+ // // // const signInResult = await this.signInCallback()
+ // // if (signInResult) {
+ // // return signInResult
+ // // } else if (sessionId) {
+ // // const client = await this.restore(sessionId, forceRefresh)
+ // // return {client}
+ // // } else {
+ // // // TODO: we could restore any session from the store ?
+ // // }
+ // }
+
+ async signIn(input: string, options?: OAuthAuthorizeOptions) {
+ console.log(options)
+ return await this.authorize(input, options)
+ }
+
+ async signInCallback(callback: string) {
+ // const redirectUri = new URL(this.clientMetadata.redirect_uris[0])
+ // if (location.pathname !== redirectUri.pathname) return null
+ //
+ const params = new URL(callback).searchParams
+ // Only if the query string contains oauth callback params
+ if (
+ !params.has('iss') ||
+ !params.has('state') ||
+ !(params.has('code') || params.has('error'))
+ ) {
+ console.log('no')
+ return null
+ } else {
+ console.log('has!')
+ }
+ //
+ // // Replace the current history entry without the query string (this will
+ // // prevent this 'if' branch to run again if the user refreshes the page)
+ // history.replaceState(null, '', location.pathname)
+ //
+ // return this.callback(params)
+ // .then(async result => {
+ // if (result.state?.startsWith(POPUP_KEY_PREFIX)) {
+ // const stateKey = result.state.slice(POPUP_KEY_PREFIX.length)
+ //
+ // await this.popupStore.set(stateKey, {
+ // status: 'fulfilled',
+ // value: result.client.sessionId,
+ // })
+ //
+ // window.close() // continued in signInPopup
+ // throw new Error('Login complete, please close the popup window.')
+ // }
+ //
+ // return result
+ // })
+ // .catch(async err => {
+ // // TODO: Throw a proper error from parent class to actually detect
+ // // oauth authorization errors
+ // const state = typeof (err as any)?.state
+ // if (typeof state === 'string' && state?.startsWith(POPUP_KEY_PREFIX)) {
+ // const stateKey = state.slice(POPUP_KEY_PREFIX.length)
+ //
+ // await this.popupStore.set(stateKey, {
+ // status: 'rejected',
+ // reason: err,
+ // })
+ //
+ // window.close() // continued in signInPopup
+ // throw new Error('Login complete, please close the popup window.')
+ // }
+ //
+ // throw err
+ // })
+ }
+}
diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.ts
new file mode 100644
index 0000000000..0e6468d9ea
--- /dev/null
+++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.ts
@@ -0,0 +1 @@
+export * from '@atproto/oauth-client-browser/src/browser-oauth-client-factory'
diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts
new file mode 100644
index 0000000000..c85275f343
--- /dev/null
+++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.native.ts
@@ -0,0 +1,210 @@
+import {GenericStore, Value} from '@atproto/caching'
+import {DidDocument} from '@atproto/did'
+import {ResolvedHandle} from '@atproto/handle-resolver'
+import {Key} from '@atproto/jwk'
+import {WebcryptoKey} from '@atproto/jwk-webcrypto'
+import {InternalStateData, Session, TokenSet} from '@atproto/oauth-client'
+import {OAuthServerMetadata} from '@atproto/oauth-server-metadata'
+import Storage from '@react-native-async-storage/async-storage'
+
+type Item = {
+ value: string
+ expiresAt: null | Date
+}
+
+type EncodedKey = {
+ keyId: string
+ keyPair: CryptoKey
+}
+
+function encodeKey(key: Key): EncodedKey {
+ return {
+ keyId: key.kid,
+ keyPair: key.cryptoKeyPair,
+ }
+}
+
+async function decodeKey(encoded: EncodedKey): Promise {
+ return WebcryptoKey.fromKeypair(encoded.keyId, encoded.keyPair)
+}
+
+export type Schema = {
+ state: Item<{
+ dpopKey: EncodedKey
+
+ iss: string
+ nonce: string
+ verifier?: string
+ appState?: string
+ }>
+ session: Item<{
+ dpopKey: EncodedKey
+ tokenSet: TokenSet
+ }>
+
+ didCache: Item
+ dpopNonceCache: Item
+ handleCache: Item
+ metadataCache: Item
+}
+
+export type DatabaseStore = GenericStore & {
+ getKeys: () => Promise
+}
+
+const STORES = [
+ 'state',
+ 'session',
+
+ 'didCache',
+ 'dpopNonceCache',
+ 'handleCache',
+ 'metadataCache',
+] as const
+
+export class RNOAuthDatabase {
+ async delete(key: string) {
+ await Storage.removeItem(key)
+ }
+
+ protected createStore(
+ dbName: N,
+ {
+ encode,
+ decode,
+ maxAge,
+ }: {
+ encode: (value: V) => Schema[N]['value'] | PromiseLike
+ decode: (encoded: Schema[N]['value']) => V | PromiseLike
+ maxAge?: number
+ },
+ ): DatabaseStore {
+ return {
+ get: async key => {
+ const itemJson = await Storage.getItem(`${dbName}.${key}`)
+ if (itemJson == null) return undefined
+
+ const item = JSON.parse(itemJson) as Schema[N]
+
+ // Too old, proactively delete
+ if (item.expiresAt != null && item.expiresAt < new Date()) {
+ await this.delete(`${dbName}.${key}`)
+ return undefined
+ }
+
+ // Item found and valid. Decode
+ return decode(item.value)
+ },
+
+ getKeys: async () => {
+ const keys = await Storage.getAllKeys()
+ return keys.filter(key => key.startsWith(`${dbName}.`)) as string[]
+ },
+
+ set: async (key, value) => {
+ const item = {
+ value: await encode(value),
+ expiresAt: maxAge == null ? null : new Date(Date.now() + maxAge),
+ } as Schema[N]
+
+ await Storage.setItem(`${dbName}.${key}`, JSON.stringify(item))
+ },
+
+ del: async key => {
+ await this.delete(`${dbName}.${key}`)
+ },
+ }
+ }
+
+ getSessionStore(): DatabaseStore {
+ return this.createStore('session', {
+ encode: ({dpopKey, ...session}) => ({
+ ...session,
+ dpopKey: encodeKey(dpopKey),
+ }),
+ decode: async ({dpopKey, ...encoded}) => ({
+ ...encoded,
+ dpopKey: await decodeKey(dpopKey),
+ }),
+ })
+ }
+
+ getStateStore(): DatabaseStore {
+ return this.createStore('state', {
+ encode: ({dpopKey, ...session}) => ({
+ ...session,
+ dpopKey: encodeKey(dpopKey),
+ }),
+ decode: async ({dpopKey, ...encoded}) => ({
+ ...encoded,
+ dpopKey: await decodeKey(dpopKey),
+ }),
+ })
+ }
+
+ getDpopNonceCache(): undefined | DatabaseStore {
+ return this.createStore('dpopNonceCache', {
+ // No time limit. It is better to try with a potentially outdated nonce
+ // and potentially succeed rather than make requests without a nonce and
+ // 100% fail.
+ encode: value => value,
+ decode: encoded => encoded,
+ })
+ }
+
+ getDidCache(): undefined | DatabaseStore {
+ return this.createStore('didCache', {
+ maxAge: 60e3,
+ encode: value => value,
+ decode: encoded => encoded,
+ })
+ }
+
+ getHandleCache(): undefined | DatabaseStore {
+ return this.createStore('handleCache', {
+ maxAge: 60e3,
+ encode: value => value,
+ decode: encoded => encoded,
+ })
+ }
+
+ getMetadataCache(): undefined | DatabaseStore {
+ return this.createStore('metadataCache', {
+ maxAge: 60e3,
+ encode: value => value,
+ decode: encoded => encoded,
+ })
+ }
+
+ async cleanup() {
+ await Promise.all(
+ STORES.map(
+ async storeName =>
+ [
+ storeName,
+ await tx
+ .objectStore(storeName)
+ .index('expiresAt')
+ .getAllKeys(query),
+ ] as const,
+ ),
+ )
+
+ const storesWithInvalidKeys = res.filter(r => r[1].length > 0)
+
+ await db.transaction(
+ storesWithInvalidKeys.map(r => r[0]),
+ 'readwrite',
+ tx =>
+ Promise.all(
+ storesWithInvalidKeys.map(async ([name, keys]) =>
+ tx.objectStore(name).delete(keys),
+ ),
+ ),
+ )
+ }
+
+ async [Symbol.asyncDispose]() {
+ await this.cleanup()
+ }
+}
diff --git a/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.ts b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.ts
new file mode 100644
index 0000000000..1fc298076b
--- /dev/null
+++ b/modules/expo-bluesky-oauth-client/src/react-native-oauth-database.ts
@@ -0,0 +1 @@
+export * from '@atproto/oauth-client-browser/src/browser-oauth-database'
diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts b/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts
new file mode 100644
index 0000000000..435e566cf5
--- /dev/null
+++ b/modules/expo-bluesky-oauth-client/src/react-native-store-with-key.ts
@@ -0,0 +1,51 @@
+import {GenericStore, Value} from '@atproto/caching'
+import {Jwk} from '@atproto/jwk'
+
+import {ReactNativeKey} from './react-native-key'
+import {ReactNativeStore} from './react-native-store'
+
+type ExposedValue = Value & {dpopKey: ReactNativeKey}
+type StoredValue = Omit & {
+ dpopKey: Jwk
+}
+
+/**
+ * Uses a {@link ReactNativeStore} to store values that contain a
+ * {@link ReactNativeKey} as `dpopKey` property. This works by serializing the
+ * {@link Key} to a JWK before storing it, and deserializing it back to a
+ * {@link ReactNativeKey} when retrieving the value.
+ */
+export class ReactNativeStoreWithKey
+ implements GenericStore
+{
+ internalStore: ReactNativeStore>
+
+ constructor(
+ protected valueExpiresAt: (value: StoredValue) => null | Date,
+ ) {
+ this.internalStore = new ReactNativeStore(valueExpiresAt)
+ }
+
+ async set(key: string, value: V): Promise {
+ const {dpopKey, ...rest} = value
+ if (!dpopKey.privateJwk) throw new Error('dpopKey.privateJwk is required')
+ await this.internalStore.set(key, {
+ ...rest,
+ dpopKey: dpopKey.privateJwk,
+ })
+ }
+
+ async get(key: string): Promise {
+ const value = await this.internalStore.get(key)
+ if (!value) return undefined
+
+ return {
+ ...value,
+ dpopKey: new ReactNativeKey(value.dpopKey),
+ } as V
+ }
+
+ async del(key: string): Promise {
+ await this.internalStore.del(key)
+ }
+}
diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store.ts b/modules/expo-bluesky-oauth-client/src/react-native-store.ts
new file mode 100644
index 0000000000..0a3d186d07
--- /dev/null
+++ b/modules/expo-bluesky-oauth-client/src/react-native-store.ts
@@ -0,0 +1,25 @@
+import {GenericStore, Value} from '@atproto/caching'
+import Storage from '@react-native-async-storage/async-storage'
+
+export class ReactNativeStore
+ implements GenericStore
+{
+ constructor(protected valueExpiresAt: (value: V) => null | Date) {
+ throw new Error('Not implemented')
+ }
+
+ async get(key: string): Promise {
+ const itemJson = await Storage.getItem(key)
+ if (itemJson == null) return undefined
+
+ return JSON.parse(itemJson) as V
+ }
+
+ async set(key: string, value: V): Promise {
+ await Storage.setItem(key, JSON.stringify(value))
+ }
+
+ async del(key: string): Promise {
+ await Storage.removeItem(key)
+ }
+}
diff --git a/modules/expo-bluesky-oauth-client/src/react-native-store.web.ts b/modules/expo-bluesky-oauth-client/src/react-native-store.web.ts
new file mode 100644
index 0000000000..b0aef4a5db
--- /dev/null
+++ b/modules/expo-bluesky-oauth-client/src/react-native-store.web.ts
@@ -0,0 +1 @@
+export * from '@atproto/oauth-client-browser/src/indexed-db-store'
diff --git a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx
index a91aebd4dc..21a2b9fb26 100644
--- a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx
+++ b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx
@@ -1,5 +1,6 @@
-import {requireNativeViewManager} from 'expo-modules-core'
import * as React from 'react'
+import {requireNativeViewManager} from 'expo-modules-core'
+
import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
const NativeView: React.ComponentType =
diff --git a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx
index 93e69333fd..7eb52b78bd 100644
--- a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx
+++ b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.tsx
@@ -1,4 +1,5 @@
import React from 'react'
+
import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
export function ExpoScrollForwarderView({
children,
diff --git a/package.json b/package.json
index 7b4e454514..895d164568 100644
--- a/package.json
+++ b/package.json
@@ -136,6 +136,7 @@
"expo-web-browser": "~12.8.2",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
+ "jose": "^5.2.4",
"js-sha256": "^0.9.0",
"jwt-decode": "^4.0.0",
"lande": "^1.0.10",
@@ -189,7 +190,7 @@
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"zeego": "^1.6.2",
- "zod": "^3.20.2"
+ "zod": "^3.22.4"
},
"devDependencies": {
"@atproto/dev-env": "^0.3.5",
@@ -263,7 +264,8 @@
},
"resolutions": {
"@types/react": "^18",
- "**/zeed-dom": "0.10.9"
+ "**/zeed-dom": "0.10.9",
+ "browserify-sign": "4.2.2"
},
"jest": {
"preset": "jest-expo/ios",
diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts
new file mode 100644
index 0000000000..391ca85059
--- /dev/null
+++ b/src/lib/oauth.ts
@@ -0,0 +1,12 @@
+import {isWeb} from 'platform/detection'
+
+export const OAUTH_CLIENT_ID = 'http://localhost/'
+export const OAUTH_REDIRECT_URI = 'http://127.0.0.1:2583/'
+export const OAUTH_SCOPE = 'openid profile email phone offline_access'
+export const OAUTH_GRANT_TYPES = [
+ 'authorization_code',
+ 'refresh_token',
+] as const
+export const OAUTH_RESPONSE_TYPES = ['code', 'code id_token'] as const
+export const DPOP_BOUND_ACCESS_TOKENS = true
+export const OAUTH_APPLICATION_TYPE = isWeb ? 'web' : 'native' // TODO what should we put here for native
diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx
index ec30bab4a8..e69de29bb2 100644
--- a/src/screens/Login/ForgotPasswordForm.tsx
+++ b/src/screens/Login/ForgotPasswordForm.tsx
@@ -1,184 +0,0 @@
-import React, {useEffect, useState} from 'react'
-import {ActivityIndicator, Keyboard, View} from 'react-native'
-import {ComAtprotoServerDescribeServer} from '@atproto/api'
-import {BskyAgent} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import * as EmailValidator from 'email-validator'
-
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {isNetworkError} from '#/lib/strings/errors'
-import {cleanError} from '#/lib/strings/errors'
-import {logger} from '#/logger'
-import {atoms as a, useTheme} from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
-import {FormError} from '#/components/forms/FormError'
-import {HostingProvider} from '#/components/forms/HostingProvider'
-import * as TextField from '#/components/forms/TextField'
-import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
-import {Text} from '#/components/Typography'
-import {FormContainer} from './FormContainer'
-
-type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
-
-export const ForgotPasswordForm = ({
- error,
- serviceUrl,
- serviceDescription,
- setError,
- setServiceUrl,
- onPressBack,
- onEmailSent,
-}: {
- error: string
- serviceUrl: string
- serviceDescription: ServiceDescription | undefined
- setError: (v: string) => void
- setServiceUrl: (v: string) => void
- onPressBack: () => void
- onEmailSent: () => void
-}) => {
- const t = useTheme()
- const [isProcessing, setIsProcessing] = useState(false)
- const [email, setEmail] = useState('')
- const {screen} = useAnalytics()
- const {_} = useLingui()
-
- useEffect(() => {
- screen('Signin:ForgotPassword')
- }, [screen])
-
- const onPressSelectService = React.useCallback(() => {
- Keyboard.dismiss()
- }, [])
-
- const onPressNext = async () => {
- if (!EmailValidator.validate(email)) {
- return setError(_(msg`Your email appears to be invalid.`))
- }
-
- setError('')
- setIsProcessing(true)
-
- try {
- const agent = new BskyAgent({service: serviceUrl})
- await agent.com.atproto.server.requestPasswordReset({email})
- onEmailSent()
- } catch (e: any) {
- const errMsg = e.toString()
- logger.warn('Failed to request password reset', {error: e})
- setIsProcessing(false)
- if (isNetworkError(e)) {
- setError(
- _(
- msg`Unable to contact your service. Please check your Internet connection.`,
- ),
- )
- } else {
- setError(cleanError(errMsg))
- }
- }
- }
-
- return (
- Reset password}>
-
-
- Hosting provider
-
-
-
-
-
- Email address
-
-
-
-
-
-
-
-
-
- Enter the email you used to create your account. We'll send you a
- "reset code" so you can set a new password.
-
-
-
-
-
-
-
-
- {!serviceDescription || isProcessing ? (
-
- ) : (
-
- )}
- {!serviceDescription || isProcessing ? (
-
- Processing...
-
- ) : undefined}
-
-
-
-
-
- )
-}
diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx
index 17fc323688..6f20354be0 100644
--- a/src/screens/Login/LoginForm.tsx
+++ b/src/screens/Login/LoginForm.tsx
@@ -1,34 +1,16 @@
-import React, {useRef, useState} from 'react'
-import {
- ActivityIndicator,
- Keyboard,
- LayoutAnimation,
- TextInput,
- View,
-} from 'react-native'
-import {
- ComAtprotoServerCreateSession,
- ComAtprotoServerDescribeServer,
-} from '@atproto/api'
+import React from 'react'
+import {Keyboard, View} from 'react-native'
+import {ComAtprotoServerDescribeServer} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
-import {isNetworkError} from '#/lib/strings/errors'
-import {cleanError} from '#/lib/strings/errors'
-import {createFullHandle} from '#/lib/strings/handles'
-import {logger} from '#/logger'
-import {useSessionApi} from '#/state/session'
-import {atoms as a, useTheme} from '#/alf'
-import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import {useLogin} from '#/screens/Login/hooks/useLogin'
+import {atoms as a} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
import {FormError} from '#/components/forms/FormError'
import {HostingProvider} from '#/components/forms/HostingProvider'
import * as TextField from '#/components/forms/TextField'
-import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
-import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
-import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
import {FormContainer} from './FormContainer'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@@ -37,113 +19,26 @@ export const LoginForm = ({
error,
serviceUrl,
serviceDescription,
- initialHandle,
- setError,
setServiceUrl,
- onPressRetryConnect,
onPressBack,
- onPressForgotPassword,
}: {
error: string
serviceUrl: string
serviceDescription: ServiceDescription | undefined
- initialHandle: string
setError: (v: string) => void
setServiceUrl: (v: string) => void
onPressRetryConnect: () => void
onPressBack: () => void
- onPressForgotPassword: () => void
}) => {
const {track} = useAnalytics()
- const t = useTheme()
- const [isProcessing, setIsProcessing] = useState(false)
- const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
- useState(false)
- const [identifier, setIdentifier] = useState(initialHandle)
- const [password, setPassword] = useState('')
- const [authFactorToken, setAuthFactorToken] = useState('')
- const passwordInputRef = useRef(null)
const {_} = useLingui()
- const {login} = useSessionApi()
+ const {openAuthSession} = useLogin()
const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss()
track('Signin:PressedSelectService')
}, [track])
- const onPressNext = async () => {
- if (isProcessing) return
- Keyboard.dismiss()
- LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
- setError('')
- setIsProcessing(true)
-
- try {
- // try to guess the handle if the user just gave their own username
- let fullIdent = identifier
- if (
- !identifier.includes('@') && // not an email
- !identifier.includes('.') && // not a domain
- serviceDescription &&
- serviceDescription.availableUserDomains.length > 0
- ) {
- let matched = false
- for (const domain of serviceDescription.availableUserDomains) {
- if (fullIdent.endsWith(domain)) {
- matched = true
- }
- }
- if (!matched) {
- fullIdent = createFullHandle(
- identifier,
- serviceDescription.availableUserDomains[0],
- )
- }
- }
-
- // TODO remove double login
- await login(
- {
- service: serviceUrl,
- identifier: fullIdent,
- password,
- authFactorToken: authFactorToken.trim(),
- },
- 'LoginForm',
- )
- } catch (e: any) {
- const errMsg = e.toString()
- LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
- setIsProcessing(false)
- if (
- e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError
- ) {
- setIsAuthFactorTokenNeeded(true)
- } else if (errMsg.includes('Token is invalid')) {
- logger.debug('Failed to login due to invalid 2fa token', {
- error: errMsg,
- })
- setError(_(msg`Invalid 2FA confirmation code.`))
- } else if (errMsg.includes('Authentication Required')) {
- logger.debug('Failed to login due to invalid credentials', {
- error: errMsg,
- })
- setError(_(msg`Invalid username or password`))
- } else if (isNetworkError(e)) {
- logger.warn('Failed to login due to network error', {error: errMsg})
- setError(
- _(
- msg`Unable to contact your service. Please check your Internet connection.`,
- ),
- )
- } else {
- logger.warn('Failed to login', {error: errMsg})
- setError(cleanError(errMsg))
- }
- }
- }
-
- const isReady = !!serviceDescription && !!identifier && !!password
return (
Sign in}>
@@ -156,115 +51,8 @@ export const LoginForm = ({
onOpenDialog={onPressSelectService}
/>
-
-
- Account
-
-
-
-
- {
- passwordInputRef.current?.focus()
- }}
- blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
- value={identifier}
- onChangeText={str =>
- setIdentifier((str || '').toLowerCase().trim())
- }
- editable={!isProcessing}
- accessibilityHint={_(
- msg`Input the username or email address you used at signup`,
- )}
- />
-
-
-
-
-
-
-
-
-
- {isAuthFactorTokenNeeded && (
-
-
- 2FA Confirmation
-
-
-
-
-
-
- Check your email for a login code and enter it here.
-
-
- )}
-
+
-
- {!serviceDescription && error ? (
-
- ) : !serviceDescription ? (
- <>
-
-
- Connecting...
-
- >
- ) : isReady ? (
-
- ) : undefined}
+
)
diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx
deleted file mode 100644
index 5407f3f1e3..0000000000
--- a/src/screens/Login/PasswordUpdatedForm.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, {useEffect} from 'react'
-import {View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {atoms as a, useBreakpoints} from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
-import {Text} from '#/components/Typography'
-import {FormContainer} from './FormContainer'
-
-export const PasswordUpdatedForm = ({
- onPressNext,
-}: {
- onPressNext: () => void
-}) => {
- const {screen} = useAnalytics()
- const {_} = useLingui()
- const {gtMobile} = useBreakpoints()
-
- useEffect(() => {
- screen('Signin:PasswordUpdatedForm')
- }, [screen])
-
- return (
-
-
- Password updated!
-
-
- You can now sign in with your new password.
-
-
-
-
-
- )
-}
diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx
index 88f7ec5416..e69de29bb2 100644
--- a/src/screens/Login/SetNewPasswordForm.tsx
+++ b/src/screens/Login/SetNewPasswordForm.tsx
@@ -1,192 +0,0 @@
-import React, {useEffect, useState} from 'react'
-import {ActivityIndicator, View} from 'react-native'
-import {BskyAgent} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {isNetworkError} from '#/lib/strings/errors'
-import {cleanError} from '#/lib/strings/errors'
-import {checkAndFormatResetCode} from '#/lib/strings/password'
-import {logger} from '#/logger'
-import {atoms as a, useTheme} from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
-import {FormError} from '#/components/forms/FormError'
-import * as TextField from '#/components/forms/TextField'
-import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
-import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
-import {Text} from '#/components/Typography'
-import {FormContainer} from './FormContainer'
-
-export const SetNewPasswordForm = ({
- error,
- serviceUrl,
- setError,
- onPressBack,
- onPasswordSet,
-}: {
- error: string
- serviceUrl: string
- setError: (v: string) => void
- onPressBack: () => void
- onPasswordSet: () => void
-}) => {
- const {screen} = useAnalytics()
- const {_} = useLingui()
- const t = useTheme()
-
- useEffect(() => {
- screen('Signin:SetNewPasswordForm')
- }, [screen])
-
- const [isProcessing, setIsProcessing] = useState(false)
- const [resetCode, setResetCode] = useState('')
- const [password, setPassword] = useState('')
-
- const onPressNext = async () => {
- // Check that the code is correct. We do this again just incase the user enters the code after their pw and we
- // don't get to call onBlur first
- const formattedCode = checkAndFormatResetCode(resetCode)
- // TODO Better password strength check
- if (!formattedCode || !password) {
- setError(
- _(
- msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
- ),
- )
- return
- }
-
- setError('')
- setIsProcessing(true)
-
- try {
- const agent = new BskyAgent({service: serviceUrl})
- await agent.com.atproto.server.resetPassword({
- token: formattedCode,
- password,
- })
- onPasswordSet()
- } catch (e: any) {
- const errMsg = e.toString()
- logger.warn('Failed to set new password', {error: e})
- setIsProcessing(false)
- if (isNetworkError(e)) {
- setError(
- _(
- msg`Unable to contact your service. Please check your Internet connection.`,
- ),
- )
- } else {
- setError(cleanError(errMsg))
- }
- }
- }
-
- const onBlur = () => {
- const formattedCode = checkAndFormatResetCode(resetCode)
- if (!formattedCode) {
- setError(
- _(
- msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
- ),
- )
- return
- }
- setResetCode(formattedCode)
- }
-
- return (
- Set new password}>
-
-
- You will receive an email with a "reset code." Enter that code here,
- then enter your new password.
-
-
-
-
- Reset code
-
-
- setError('')}
- onBlur={onBlur}
- editable={!isProcessing}
- accessibilityHint={_(
- msg`Input code sent to your email for password reset`,
- )}
- />
-
-
-
-
- New password
-
-
-
-
-
-
-
-
-
-
-
- {isProcessing ? (
-
- ) : (
-
- )}
- {isProcessing ? (
-
- Updating...
-
- ) : undefined}
-
-
- )
-}
diff --git a/src/screens/Login/hooks/package.json b/src/screens/Login/hooks/package.json
new file mode 100644
index 0000000000..3a1fb0a9b1
--- /dev/null
+++ b/src/screens/Login/hooks/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "hooks",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/bluesky-social/social-app.git"
+ },
+ "private": true
+}
diff --git a/src/screens/Login/hooks/useLogin.ts b/src/screens/Login/hooks/useLogin.ts
new file mode 100644
index 0000000000..1aa01b040d
--- /dev/null
+++ b/src/screens/Login/hooks/useLogin.ts
@@ -0,0 +1,48 @@
+import React from 'react'
+import * as Browser from 'expo-web-browser'
+
+import {
+ DPOP_BOUND_ACCESS_TOKENS,
+ OAUTH_APPLICATION_TYPE,
+ OAUTH_CLIENT_ID,
+ OAUTH_GRANT_TYPES,
+ OAUTH_REDIRECT_URI,
+ OAUTH_RESPONSE_TYPES,
+ OAUTH_SCOPE,
+} from 'lib/oauth'
+import {RNOAuthClientFactory} from '../../../../modules/expo-bluesky-oauth-client/src/react-native-oauth-client-factory.native'
+
+// Service URL here is just a placeholder, this isn't how it will actually work
+export function useLogin() {
+ const openAuthSession = React.useCallback(async () => {
+ const oauthFactory = new RNOAuthClientFactory({
+ clientMetadata: {
+ client_id: OAUTH_CLIENT_ID,
+ redirect_uris: [OAUTH_REDIRECT_URI],
+ grant_types: OAUTH_GRANT_TYPES,
+ response_types: OAUTH_RESPONSE_TYPES,
+ scope: OAUTH_SCOPE,
+ dpop_bound_access_tokens: DPOP_BOUND_ACCESS_TOKENS,
+ application_type: OAUTH_APPLICATION_TYPE,
+ },
+ fetch: global.fetch,
+ })
+
+ const url = await oauthFactory.signIn('http://localhost:2583/')
+
+ console.log(url.href)
+
+ const authSession = await Browser.openAuthSessionAsync(
+ url.href,
+ OAUTH_REDIRECT_URI,
+ )
+
+ if (authSession.type !== 'success') {
+ return
+ }
+ }, [])
+
+ return {
+ openAuthSession,
+ }
+}
diff --git a/src/screens/Login/hooks/useLogin.web.ts b/src/screens/Login/hooks/useLogin.web.ts
new file mode 100644
index 0000000000..6c16bc1810
--- /dev/null
+++ b/src/screens/Login/hooks/useLogin.web.ts
@@ -0,0 +1,13 @@
+import React from 'react'
+
+export function useLogin(serviceUrl: string | undefined) {
+ const openAuthSession = React.useCallback(async () => {
+ if (!serviceUrl) return
+
+ window.location.href = serviceUrl
+ }, [serviceUrl])
+
+ return {
+ openAuthSession,
+ }
+}
diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx
index 1fce63d298..42b355a730 100644
--- a/src/screens/Login/index.tsx
+++ b/src/screens/Login/index.tsx
@@ -4,17 +4,13 @@ import {LayoutAnimationConfig} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {DEFAULT_SERVICE} from '#/lib/constants'
import {logger} from '#/logger'
import {useServiceQuery} from '#/state/queries/service'
import {SessionAccount, useSession} from '#/state/session'
import {useLoggedOutView} from '#/state/shell/logged-out'
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
-import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm'
import {LoginForm} from '#/screens/Login/LoginForm'
-import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm'
-import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm'
import {atoms as a} from '#/alf'
import {ChooseAccountForm} from './ChooseAccountForm'
import {ScreenTransition} from './ScreenTransition'
@@ -22,16 +18,12 @@ import {ScreenTransition} from './ScreenTransition'
enum Forms {
Login,
ChooseAccount,
- ForgotPassword,
- SetNewPassword,
- PasswordUpdated,
}
export const Login = ({onPressBack}: {onPressBack: () => void}) => {
const {_} = useLingui()
const {accounts} = useSession()
- const {track} = useAnalytics()
const {requestedAccountSwitchTo} = useLoggedOutView()
const requestedAccount = accounts.find(
acc => acc.did === requestedAccountSwitchTo,
@@ -41,9 +33,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
const [serviceUrl, setServiceUrl] = React.useState(
requestedAccount?.service || DEFAULT_SERVICE,
)
- const [initialHandle, setInitialHandle] = React.useState(
- requestedAccount?.handle || '',
- )
const [currentForm, setCurrentForm] = React.useState(
requestedAccount
? Forms.Login
@@ -62,7 +51,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
if (account?.service) {
setServiceUrl(account.service)
}
- setInitialHandle(account?.handle || '')
+ // TODO set the service URL. We really need to fix this though in general
setCurrentForm(Forms.Login)
}
@@ -86,11 +75,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
}
}, [serviceError, serviceUrl, _])
- const onPressForgotPassword = () => {
- track('Signin:PressedForgotPassword')
- setCurrentForm(Forms.ForgotPassword)
- }
-
let content = null
let title = ''
let description = ''
@@ -104,13 +88,11 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
error={error}
serviceUrl={serviceUrl}
serviceDescription={serviceDescription}
- initialHandle={initialHandle}
setError={setError}
setServiceUrl={setServiceUrl}
onPressBack={() =>
accounts.length ? gotoForm(Forms.ChooseAccount) : onPressBack()
}
- onPressForgotPassword={onPressForgotPassword}
onPressRetryConnect={refetchService}
/>
)
@@ -125,41 +107,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
/>
)
break
- case Forms.ForgotPassword:
- title = _(msg`Forgot Password`)
- description = _(msg`Let's get your password reset!`)
- content = (
- gotoForm(Forms.Login)}
- onEmailSent={() => gotoForm(Forms.SetNewPassword)}
- />
- )
- break
- case Forms.SetNewPassword:
- title = _(msg`Forgot Password`)
- description = _(msg`Let's get your password reset!`)
- content = (
- gotoForm(Forms.ForgotPassword)}
- onPasswordSet={() => gotoForm(Forms.PasswordUpdated)}
- />
- )
- break
- case Forms.PasswordUpdated:
- title = _(msg`Password updated`)
- description = _(msg`You can now sign in with your new password.`)
- content = (
- gotoForm(Forms.Login)} />
- )
- break
}
return (
diff --git a/webpack.config.js b/webpack.config.js
index 6f1de3b8b7..5aa2d47f5b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,3 +1,5 @@
+const fs = require('fs')
+const path = require('path')
const createExpoWebpackConfigAsync = require('@expo/webpack-config')
const {withAlias} = require('@expo/webpack-config/addons')
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
@@ -22,6 +24,25 @@ module.exports = async function (env, argv) {
'react-native$': 'react-native-web',
'react-native-webview': 'react-native-web-webview',
})
+
+ if (process.env.ATPROTO_ROOT) {
+ const atprotoRoot = path.resolve(process.cwd(), process.env.ATPROTO_ROOT)
+ const atprotoPackages = path.join(atprotoRoot, 'packages')
+
+ config = withAlias(
+ config,
+ Object.fromEntries(
+ fs
+ .readdirSync(atprotoPackages)
+ .map(pkgName => [pkgName, path.join(atprotoPackages, pkgName)])
+ .filter(([_, pkgPath]) =>
+ fs.existsSync(path.join(pkgPath, 'package.json')),
+ )
+ .map(([pkgName, pkgPath]) => [`@atproto/${pkgName}`, pkgPath]),
+ ),
+ )
+ }
+
config.module.rules = [
...(config.module.rules || []),
reactNativeWebWebviewConfiguration,
diff --git a/yarn.lock b/yarn.lock
index f9e8644cb1..63b027fed6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8616,6 +8616,15 @@ asap@~2.0.3, asap@~2.0.6:
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
+asn1.js@^4.10.1:
+ version "4.10.1"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
+ integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
asn1.js@^5.0.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
@@ -9140,6 +9149,11 @@ bn.js@^4.0.0, bn.js@^4.11.8, bn.js@^4.11.9:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
+bn.js@^5.0.0, bn.js@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
+ integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
+
body-parser@1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
@@ -9236,6 +9250,41 @@ browser-process-hrtime@^1.0.0:
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
+browserify-aes@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+ integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-rsa@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d"
+ integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==
+ dependencies:
+ bn.js "^5.0.0"
+ randombytes "^2.0.1"
+
+browserify-sign@4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e"
+ integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==
+ dependencies:
+ bn.js "^5.2.1"
+ browserify-rsa "^4.1.0"
+ create-hash "^1.2.0"
+ create-hmac "^1.1.7"
+ elliptic "^6.5.4"
+ inherits "^2.0.4"
+ parse-asn1 "^5.1.6"
+ readable-stream "^3.6.2"
+ safe-buffer "^5.2.1"
+
browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.21.9:
version "4.21.10"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0"
@@ -9281,6 +9330,11 @@ buffer-writer@2.0.0:
resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+ integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==
+
buffer@5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
@@ -9640,6 +9694,14 @@ ci-info@^3.2.0, ci-info@^3.3.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
cjs-module-lexer@^1.0.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107"
@@ -10080,6 +10142,29 @@ cosmiconfig@^8.0.0:
parse-json "^5.2.0"
path-type "^4.0.0"
+create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+ integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ md5.js "^1.3.4"
+ ripemd160 "^2.0.1"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.4, create-hmac@^1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+ integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
create-jest@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320"
@@ -10945,6 +11030,19 @@ elliptic@^6.4.1:
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
+elliptic@^6.5.4:
+ version "6.5.5"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.5.tgz#c715e09f78b6923977610d4c2346d6ce22e6dded"
+ integrity sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==
+ dependencies:
+ bn.js "^4.11.9"
+ brorand "^1.1.0"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.1"
+ inherits "^2.0.4"
+ minimalistic-assert "^1.0.1"
+ minimalistic-crypto-utils "^1.0.1"
+
email-validator@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed"
@@ -11644,6 +11742,14 @@ events@3.3.0, events@^3.2.0, events@^3.3.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
exec-async@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/exec-async/-/exec-async-2.2.0.tgz#c7c5ad2eef3478d38390c6dd3acfe8af0efc8301"
@@ -13031,6 +13137,23 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
+hash-base@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
+ integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==
+ dependencies:
+ inherits "^2.0.4"
+ readable-stream "^3.6.0"
+ safe-buffer "^5.2.0"
+
+hash-base@~3.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+ integrity sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
hash.js@^1.0.0, hash.js@^1.0.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
@@ -15083,6 +15206,11 @@ jose@^5.0.1:
resolved "https://registry.yarnpkg.com/jose/-/jose-5.1.3.tgz#303959d85c51b5cb14725f930270b72be56abdca"
integrity sha512-GPExOkcMsCLBTi1YetY2LmkoY559fss0+0KVa6kOfb2YFe84nAM7Nm/XzuZozah4iHgmBGrCOHL5/cy670SBRw==
+jose@^5.2.4:
+ version "5.2.4"
+ resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.4.tgz#c0d296caeeed0b8444a8b8c3b68403d61aa4ed72"
+ integrity sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==
+
js-base64@^3.7.2:
version "3.7.5"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca"
@@ -15832,6 +15960,15 @@ md5-file@^3.2.3:
dependencies:
buffer-alloc "^1.1.0"
+md5.js@^1.3.4:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
+ integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+ safe-buffer "^5.1.2"
+
md5@^2.2.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
@@ -17061,6 +17198,18 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
+parse-asn1@^5.1.6:
+ version "5.1.7"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06"
+ integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==
+ dependencies:
+ asn1.js "^4.10.1"
+ browserify-aes "^1.2.0"
+ evp_bytestokey "^1.0.3"
+ hash-base "~3.0"
+ pbkdf2 "^3.1.2"
+ safe-buffer "^5.2.1"
+
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -17207,6 +17356,17 @@ pathe@^1.1.0:
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a"
integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==
+pbkdf2@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"
+ integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
peek-readable@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72"
@@ -18495,7 +18655,7 @@ ramda@^0.27.1:
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1"
integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==
-randombytes@^2.1.0:
+randombytes@^2.0.1, randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
@@ -18991,7 +19151,7 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@~2.3.6:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
-readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0:
+readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
@@ -19384,6 +19544,14 @@ rimraf@~2.6.2:
dependencies:
glob "^7.1.3"
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
+ integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
rn-fetch-blob@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.12.0.tgz#ec610d2f9b3f1065556b58ab9c106eeb256f3cba"
@@ -19482,7 +19650,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
+safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -19769,6 +19937,14 @@ sf-symbols-typescript@^1.0.0:
resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-1.0.0.tgz#94e9210bf27e7583f9749a0d07bd4f4937ea488f"
integrity sha512-DkS7q3nN68dEMb4E18HFPDAvyrjDZK9YAQQF2QxeFu9gp2xRDXFMF8qLJ1EmQ/qeEGQmop4lmMM1WtYJTIcCMw==
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.11"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+ integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
shallow-clone@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
@@ -22345,7 +22521,12 @@ zeego@^1.6.2:
"@radix-ui/react-dropdown-menu" "^2.0.1"
sf-symbols-typescript "^1.0.0"
-zod@^3.14.2, zod@^3.20.2, zod@^3.21.4:
+zod@^3.14.2, zod@^3.21.4:
version "3.22.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b"
integrity sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==
+
+zod@^3.22.4:
+ version "3.22.4"
+ resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
+ integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==