Age Assurance V2 (#9479)
* Age Assurance V2 * Tighten up test * Add todos for sdk migration * Align RQ versions * Use useEffect for side effect * Improve effects, memoize * Standarize on birthdate * Copy feedback * Copilot * Add support link * Reove double .. * Cleanup * Remove redirect dialog * Cleanup todos, add comments * Update splash in main template too * Mock some stuff * Exhaustive checks Co-authored-by: Samuel Newman <mozzius@protonmail.com> * Exhaustive checks Co-authored-by: Samuel Newman <mozzius@protonmail.com> * Small fix to bday handling * Add comment * onboarding style tweak sneaking this in sorry! * rm unreachable breaks * Put useIntentHandler back on web * Remove misleading success set * Align on birthdate --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -10,6 +10,9 @@ jest.mock('jwt-decode', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('../../birthdate')
|
||||
jest.mock('../../../ageAssurance/data')
|
||||
|
||||
describe('session', () => {
|
||||
it('can log in and out', () => {
|
||||
let state = getInitialState([])
|
||||
|
||||
+143
-40
@@ -1,10 +1,12 @@
|
||||
import {
|
||||
Agent as BaseAgent,
|
||||
type AppBskyActorProfile,
|
||||
type AtprotoServiceType,
|
||||
type AtpSessionData,
|
||||
type AtpSessionEvent,
|
||||
BskyAgent,
|
||||
type Did,
|
||||
type Un$Typed,
|
||||
} from '@atproto/api'
|
||||
import {type FetchHandler} from '@atproto/api/dist/agent'
|
||||
import {type SessionManager} from '@atproto/api/dist/session-manager'
|
||||
@@ -23,7 +25,13 @@ import {
|
||||
import {tryFetchGates} from '#/lib/statsig/statsig'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {logger} from '#/logger'
|
||||
import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate'
|
||||
import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders'
|
||||
import {
|
||||
prefetchAgeAssuranceData,
|
||||
setBirthdateForDid,
|
||||
setCreatedAtForDid,
|
||||
} from '#/ageAssurance/data'
|
||||
import {emitNetworkConfirmed, emitNetworkLost} from '../events'
|
||||
import {addSessionErrorLog} from './logging'
|
||||
import {
|
||||
@@ -77,9 +85,15 @@ export async function createAgentAndResume(
|
||||
}
|
||||
}
|
||||
|
||||
// after session is attached
|
||||
const aa = prefetchAgeAssuranceData({agent})
|
||||
|
||||
agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
|
||||
return agent.prepare(gates, moderation, onSessionChange)
|
||||
return agent.prepare({
|
||||
resolvers: [gates, moderation, aa],
|
||||
onSessionChange,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createAgentAndLogin(
|
||||
@@ -111,10 +125,14 @@ export async function createAgentAndLogin(
|
||||
const account = agentToSessionAccountOrThrow(agent)
|
||||
const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
|
||||
const moderation = configureModerationForAccount(agent, account)
|
||||
const aa = prefetchAgeAssuranceData({agent})
|
||||
|
||||
agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
|
||||
return agent.prepare(gates, moderation, onSessionChange)
|
||||
return agent.prepare({
|
||||
resolvers: [gates, moderation, aa],
|
||||
onSessionChange,
|
||||
})
|
||||
}
|
||||
|
||||
export async function createAgentAndCreateAccount(
|
||||
@@ -156,42 +174,122 @@ export async function createAgentAndCreateAccount(
|
||||
const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
|
||||
const moderation = configureModerationForAccount(agent, account)
|
||||
|
||||
const createdAt = new Date().toISOString()
|
||||
const birthdate = birthDate.toISOString()
|
||||
|
||||
/*
|
||||
* Since we have a race with account creation, profile creation, and AA
|
||||
* state, set these values locally to ensure sync reads. Values are written
|
||||
* to the server in the next step, so on subsequent reloads, the server will
|
||||
* be the source of truth.
|
||||
*/
|
||||
setCreatedAtForDid({did: account.did, createdAt})
|
||||
setBirthdateForDid({did: account.did, birthdate})
|
||||
snoozeBirthdateUpdateAllowedForDid(account.did)
|
||||
// do this last
|
||||
const aa = prefetchAgeAssuranceData({agent})
|
||||
|
||||
// Not awaited so that we can still get into onboarding.
|
||||
// This is OK because we won't let you toggle adult stuff until you set the date.
|
||||
if (IS_PROD_SERVICE(service)) {
|
||||
try {
|
||||
networkRetry(1, async () => {
|
||||
await agent.setPersonalDetails({birthDate: birthDate.toISOString()})
|
||||
await agent.overwriteSavedFeeds([
|
||||
{
|
||||
...DISCOVER_SAVED_FEED,
|
||||
id: TID.nextStr(),
|
||||
},
|
||||
{
|
||||
...TIMELINE_SAVED_FEED,
|
||||
id: TID.nextStr(),
|
||||
},
|
||||
])
|
||||
|
||||
if (getAge(birthDate) < 18) {
|
||||
await agent.api.com.atproto.repo.putRecord({
|
||||
repo: account.did,
|
||||
collection: 'chat.bsky.actor.declaration',
|
||||
rkey: 'self',
|
||||
record: {
|
||||
$type: 'chat.bsky.actor.declaration',
|
||||
allowIncoming: 'none',
|
||||
},
|
||||
Promise.allSettled(
|
||||
[
|
||||
networkRetry(3, () => {
|
||||
return agent.setPersonalDetails({
|
||||
birthDate: birthdate,
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch (e: any) {
|
||||
logger.error(e, {
|
||||
message: `session: createAgentAndCreateAccount failed to save personal details and feeds`,
|
||||
})
|
||||
}
|
||||
}).catch(e => {
|
||||
logger.info(`createAgentAndCreateAccount: failed to set birthDate`)
|
||||
throw e
|
||||
}),
|
||||
networkRetry(3, () => {
|
||||
return agent.upsertProfile(prev => {
|
||||
const next: Un$Typed<AppBskyActorProfile.Record> = prev || {}
|
||||
next.displayName = handle
|
||||
next.createdAt = createdAt
|
||||
return next
|
||||
})
|
||||
}).catch(e => {
|
||||
logger.info(
|
||||
`createAgentAndCreateAccount: failed to set initial profile`,
|
||||
)
|
||||
throw e
|
||||
}),
|
||||
networkRetry(1, () => {
|
||||
return agent.overwriteSavedFeeds([
|
||||
{
|
||||
...DISCOVER_SAVED_FEED,
|
||||
id: TID.nextStr(),
|
||||
},
|
||||
{
|
||||
...TIMELINE_SAVED_FEED,
|
||||
id: TID.nextStr(),
|
||||
},
|
||||
])
|
||||
}).catch(e => {
|
||||
logger.info(
|
||||
`createAgentAndCreateAccount: failed to set initial feeds`,
|
||||
)
|
||||
throw e
|
||||
}),
|
||||
getAge(birthDate) < 18 &&
|
||||
networkRetry(3, () => {
|
||||
return agent.com.atproto.repo.putRecord({
|
||||
repo: account.did,
|
||||
collection: 'chat.bsky.actor.declaration',
|
||||
rkey: 'self',
|
||||
record: {
|
||||
$type: 'chat.bsky.actor.declaration',
|
||||
allowIncoming: 'none',
|
||||
},
|
||||
})
|
||||
}).catch(e => {
|
||||
logger.info(
|
||||
`createAgentAndCreateAccount: failed to set chat declaration`,
|
||||
)
|
||||
throw e
|
||||
}),
|
||||
].filter(Boolean),
|
||||
).then(promises => {
|
||||
const rejected = promises.filter(p => p.status === 'rejected')
|
||||
if (rejected.length > 0) {
|
||||
logger.error(
|
||||
`session: createAgentAndCreateAccount failed to save personal details and feeds`,
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
agent.setPersonalDetails({birthDate: birthDate.toISOString()})
|
||||
Promise.allSettled(
|
||||
[
|
||||
networkRetry(3, () => {
|
||||
return agent.setPersonalDetails({
|
||||
birthDate: birthDate.toISOString(),
|
||||
})
|
||||
}).catch(e => {
|
||||
logger.info(`createAgentAndCreateAccount: failed to set birthDate`)
|
||||
throw e
|
||||
}),
|
||||
networkRetry(3, () => {
|
||||
return agent.upsertProfile(prev => {
|
||||
const next: Un$Typed<AppBskyActorProfile.Record> = prev || {}
|
||||
next.createdAt = prev?.createdAt || new Date().toISOString()
|
||||
return next
|
||||
})
|
||||
}).catch(e => {
|
||||
logger.info(
|
||||
`createAgentAndCreateAccount: failed to set initial profile`,
|
||||
)
|
||||
throw e
|
||||
}),
|
||||
].filter(Boolean),
|
||||
).then(promises => {
|
||||
const rejected = promises.filter(p => p.status === 'rejected')
|
||||
if (rejected.length > 0) {
|
||||
logger.error(
|
||||
`session: createAgentAndCreateAccount failed to save personal details and feeds`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -203,7 +301,10 @@ export async function createAgentAndCreateAccount(
|
||||
|
||||
agent.configureProxy(BLUESKY_PROXY_HEADER.get())
|
||||
|
||||
return agent.prepare(gates, moderation, onSessionChange)
|
||||
return agent.prepare({
|
||||
resolvers: [gates, moderation, aa],
|
||||
onSessionChange,
|
||||
})
|
||||
}
|
||||
|
||||
export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount {
|
||||
@@ -306,18 +407,20 @@ class BskyAppAgent extends BskyAgent {
|
||||
})
|
||||
}
|
||||
|
||||
async prepare(
|
||||
async prepare({
|
||||
resolvers,
|
||||
onSessionChange,
|
||||
}: {
|
||||
// Not awaited in the calling code so we can delay blocking on them.
|
||||
gates: Promise<void>,
|
||||
moderation: Promise<void>,
|
||||
resolvers: Promise<unknown>[]
|
||||
onSessionChange: (
|
||||
agent: BskyAgent,
|
||||
did: string,
|
||||
event: AtpSessionEvent,
|
||||
) => void,
|
||||
) {
|
||||
) => void
|
||||
}) {
|
||||
// There's nothing else left to do, so block on them here.
|
||||
await Promise.all([gates, moderation])
|
||||
await Promise.all(resolvers)
|
||||
|
||||
// Now the agent is ready.
|
||||
const account = agentToSessionAccountOrThrow(this)
|
||||
|
||||
@@ -24,6 +24,11 @@ import {
|
||||
type SessionApiContext,
|
||||
type SessionStateContext,
|
||||
} from '#/state/session/types'
|
||||
import {useOnboardingDispatch} from '#/state/shell/onboarding'
|
||||
import {
|
||||
clearAgeAssuranceData,
|
||||
clearAgeAssuranceDataForDid,
|
||||
} from '#/ageAssurance/data'
|
||||
|
||||
const StateContext = React.createContext<SessionStateContext>({
|
||||
accounts: [],
|
||||
@@ -91,6 +96,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const cancelPendingTask = useOneTaskAtATime()
|
||||
const [store] = React.useState(() => new SessionStore())
|
||||
const state = React.useSyncExternalStore(store.subscribe, store.getState)
|
||||
const onboardingDispatch = useOnboardingDispatch()
|
||||
|
||||
const onAgentSessionChange = React.useCallback(
|
||||
(agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {
|
||||
@@ -166,6 +172,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
logContext => {
|
||||
addSessionDebugLog({type: 'method:start', method: 'logout'})
|
||||
cancelPendingTask()
|
||||
const prevState = store.getState()
|
||||
store.dispatch({
|
||||
type: 'logged-out-current-account',
|
||||
})
|
||||
@@ -175,8 +182,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
{statsig: true},
|
||||
)
|
||||
addSessionDebugLog({type: 'method:end', method: 'logout'})
|
||||
if (prevState.currentAgentState.did) {
|
||||
clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did})
|
||||
}
|
||||
// reset onboarding flow on logout
|
||||
onboardingDispatch({type: 'skip'})
|
||||
},
|
||||
[store, cancelPendingTask],
|
||||
[store, cancelPendingTask, onboardingDispatch],
|
||||
)
|
||||
|
||||
const logoutEveryAccount = React.useCallback<
|
||||
@@ -194,12 +206,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
{statsig: true},
|
||||
)
|
||||
addSessionDebugLog({type: 'method:end', method: 'logout'})
|
||||
clearAgeAssuranceData()
|
||||
// reset onboarding flow on logout
|
||||
onboardingDispatch({type: 'skip'})
|
||||
},
|
||||
[store, cancelPendingTask],
|
||||
[store, cancelPendingTask, onboardingDispatch],
|
||||
)
|
||||
|
||||
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
|
||||
async storedAccount => {
|
||||
async (storedAccount, isSwitchingAccounts = false) => {
|
||||
addSessionDebugLog({
|
||||
type: 'method:start',
|
||||
method: 'resumeSession',
|
||||
@@ -220,8 +235,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
newAccount: account,
|
||||
})
|
||||
addSessionDebugLog({type: 'method:end', method: 'resumeSession', account})
|
||||
if (isSwitchingAccounts) {
|
||||
// reset onboarding flow on switch account
|
||||
onboardingDispatch({type: 'skip'})
|
||||
}
|
||||
},
|
||||
[store, onAgentSessionChange, cancelPendingTask],
|
||||
[store, onAgentSessionChange, cancelPendingTask, onboardingDispatch],
|
||||
)
|
||||
|
||||
const partialRefreshSession = React.useCallback<
|
||||
@@ -254,6 +273,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
accountDid: account.did,
|
||||
})
|
||||
addSessionDebugLog({type: 'method:end', method: 'removeAccount', account})
|
||||
clearAgeAssuranceDataForDid({did: account.did})
|
||||
},
|
||||
[store, cancelPendingTask],
|
||||
)
|
||||
|
||||
@@ -38,7 +38,10 @@ export type SessionApiContext = {
|
||||
logoutEveryAccount: (
|
||||
logContext: LogEvents['account:loggedOut']['logContext'],
|
||||
) => void
|
||||
resumeSession: (account: SessionAccount) => Promise<void>
|
||||
resumeSession: (
|
||||
account: SessionAccount,
|
||||
isSwitchingAccounts?: boolean,
|
||||
) => Promise<void>
|
||||
removeAccount: (account: SessionAccount) => void
|
||||
/**
|
||||
* Calls `getSession` and updates select fields on the current account and
|
||||
|
||||
Reference in New Issue
Block a user