fix(guests): don't answer feedback with a stale identity mid-invite
Last open finding from codex round 3 on #1268. Invite redemption is async and the gallery stays interactive while it runs, so a like clicked in that window resolved against the persisted identity and was filed under the wrong guest permanently. ensureIdentity() now waits on the in-flight redemption and re-reads the result before falling back to the stored identity or the prompt.
This commit is contained in:
@@ -164,6 +164,11 @@ export const GuestIdentityProvider: React.FC<GuestIdentityProviderProps> = ({
|
|||||||
// The ref keeps it to one redemption per token: `identity` is no longer in
|
// The ref keeps it to one redemption per token: `identity` is no longer in
|
||||||
// the dependency list precisely because redeeming sets it.
|
// the dependency list precisely because redeeming sets it.
|
||||||
const redeemedInviteRef = useRef<string | null>(null);
|
const redeemedInviteRef = useRef<string | null>(null);
|
||||||
|
// Redemption is async, and the gallery stays interactive while it runs. A
|
||||||
|
// like clicked in that window would otherwise resolve against the persisted
|
||||||
|
// identity and be filed under the wrong guest, permanently. ensureIdentity()
|
||||||
|
// waits on this instead.
|
||||||
|
const invitePromiseRef = useRef<Promise<void> | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (identityMode !== 'guest') return;
|
if (identityMode !== 'guest') return;
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@@ -171,7 +176,7 @@ export const GuestIdentityProvider: React.FC<GuestIdentityProviderProps> = ({
|
|||||||
if (!inviteToken || redeemedInviteRef.current === inviteToken) return;
|
if (!inviteToken || redeemedInviteRef.current === inviteToken) return;
|
||||||
redeemedInviteRef.current = inviteToken;
|
redeemedInviteRef.current = inviteToken;
|
||||||
|
|
||||||
(async () => {
|
invitePromiseRef.current = (async () => {
|
||||||
try {
|
try {
|
||||||
const response = await guestsService.redeemInvite(slug, inviteToken);
|
const response = await guestsService.redeemInvite(slug, inviteToken);
|
||||||
storeGuestIdentity(slug, response.guest, response.token);
|
storeGuestIdentity(slug, response.guest, response.token);
|
||||||
@@ -185,6 +190,8 @@ export const GuestIdentityProvider: React.FC<GuestIdentityProviderProps> = ({
|
|||||||
// Silently fail invalid invites; user will fall back to normal prompt.
|
// Silently fail invalid invites; user will fall back to normal prompt.
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('Failed to redeem invite token', error);
|
console.warn('Failed to redeem invite token', error);
|
||||||
|
} finally {
|
||||||
|
invitePromiseRef.current = null;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [slug, identityMode]);
|
}, [slug, identityMode]);
|
||||||
@@ -255,7 +262,7 @@ export const GuestIdentityProvider: React.FC<GuestIdentityProviderProps> = ({
|
|||||||
setIdentity(null);
|
setIdentity(null);
|
||||||
}, [slug]);
|
}, [slug]);
|
||||||
|
|
||||||
const ensureIdentity = useCallback((): Promise<GuestIdentity> => {
|
const ensureIdentity = useCallback(async (): Promise<GuestIdentity> => {
|
||||||
if (identityMode !== 'guest') {
|
if (identityMode !== 'guest') {
|
||||||
// In simple mode, there is no per-person identity. Return a synthetic
|
// In simple mode, there is no per-person identity. Return a synthetic
|
||||||
// "null" identity that callers will ignore.
|
// "null" identity that callers will ignore.
|
||||||
@@ -266,14 +273,26 @@ export const GuestIdentityProvider: React.FC<GuestIdentityProviderProps> = ({
|
|||||||
identifier: '',
|
identifier: '',
|
||||||
} as GuestIdentity);
|
} as GuestIdentity);
|
||||||
}
|
}
|
||||||
if (identity) return Promise.resolve(identity);
|
// An invite naming this visitor is mid-flight: wait for it rather than
|
||||||
|
// answering with the identity the browser happened to be holding.
|
||||||
|
if (invitePromiseRef.current) {
|
||||||
|
try {
|
||||||
|
await invitePromiseRef.current;
|
||||||
|
} catch {
|
||||||
|
// Redemption failed — fall through to the stored identity / prompt.
|
||||||
|
}
|
||||||
|
const redeemed = getGuestIdentity(slug);
|
||||||
|
if (redeemed) return redeemed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity) return identity;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
pendingResolvers.current.push(resolve);
|
pendingResolvers.current.push(resolve);
|
||||||
pendingRejecters.current.push(reject);
|
pendingRejecters.current.push(reject);
|
||||||
setPromptOpen(true);
|
setPromptOpen(true);
|
||||||
});
|
});
|
||||||
}, [identityMode, identity]);
|
}, [identityMode, identity, slug]);
|
||||||
|
|
||||||
const isRequired = identityMode === 'guest' && !identity;
|
const isRequired = identityMode === 'guest' && !identity;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user