Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions desktop/src/features/onboarding/communityOnboarding.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,32 @@ test("same-relay ingress resumes rather than replacing progress", () => {
assert.equal(resumed.inviteCode, "new-code");
});

test("same-relay membership recovery restarts at invite claim", () => {
const storage = createMemoryStorage();
const deniedConnect = startCommunityOnboarding(
{ source: "first-community", relayUrl: "wss://relay.example" },
storage,
new Date("2026-07-16T00:00:00Z"),
);
assert.equal(deniedConnect.stage, "connecting");

const recovered = startCommunityOnboarding(
{
source: "membership-recovery",
relayUrl: "wss://relay.example/",
inviteCode: " same-relay-code ",
policyReceipt: "policy-receipt",
},
storage,
new Date("2026-07-16T00:01:00Z"),
);

assert.equal(recovered.id, deniedConnect.id);
assert.equal(recovered.stage, "claiming");
assert.equal(recovered.inviteCode, "same-relay-code");
assert.equal(recovered.policyReceipt, "policy-receipt");
});

test("stale asynchronous updates cannot mutate a replacement transaction", () => {
const storage = createMemoryStorage();
const original = startCommunityOnboarding(
Expand Down
8 changes: 8 additions & 0 deletions desktop/src/features/onboarding/communityOnboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,16 @@ export function startCommunityOnboarding(
const relayUrl = canonicalRelayUrl(input.relayUrl);
const existing = loadCommunityOnboardingTransaction(storage);
if (existing?.relayUrl === relayUrl) {
const isMembershipRecoveryClaim =
input.source === "membership-recovery" &&
Boolean(input.inviteCode?.trim());
const updated = {
...existing,
// A same-relay membership recovery is not a passive resume: the relay
// already rejected the connect attempt, so the invite must restart the
// transaction at the claim stage. Other same-relay ingress still keeps
// in-progress profile/finalization work intact.
stage: isMembershipRecoveryClaim ? "claiming" : existing.stage,
firstCommunityPage:
input.firstCommunityPage ?? existing.firstCommunityPage,
inviteCode: input.inviteCode?.trim() || existing.inviteCode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export function CommunityOnboardingFlow({
setIsMembershipDenied(false);
update({ stage: "connecting", error: undefined });
}}
onInviteRedeemStarted={() => setIsMembershipDenied(false)}
onRetry={() => {
setIsMembershipDenied(false);
update({ stage: "connecting", error: undefined });
Expand Down
7 changes: 5 additions & 2 deletions desktop/src/features/onboarding/ui/MembershipDenied.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type MembershipDeniedProps = {
onBack: () => void;
onChangeCommunity: () => void;
onImportKey: (nsec: string) => Promise<void>;
onInviteRedeemStarted?: () => void;
onRetry: () => void;
pubkey: string;
};
Expand All @@ -26,6 +27,7 @@ export function MembershipDenied({
onBack,
onChangeCommunity,
onImportKey,
onInviteRedeemStarted,
onRetry,
pubkey,
}: MembershipDeniedProps) {
Expand Down Expand Up @@ -86,14 +88,15 @@ export function MembershipDenied({

const handleInviteRedeem = React.useCallback(
(relayWsUrl: string, code: string, policyReceipt?: string) => {
communityOnboarding.start({
const started = communityOnboarding.start({
source: "membership-recovery",
relayUrl: relayWsUrl,
inviteCode: code,
policyReceipt,
});
if (started) onInviteRedeemStarted?.();
},
[communityOnboarding],
[communityOnboarding, onInviteRedeemStarted],
);

return (
Expand Down
4 changes: 4 additions & 0 deletions desktop/src/features/onboarding/ui/OnboardingFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,10 @@ export function OnboardingFlow({
}}
onChangeCommunity={() => setIsCommunityChangeOpen(true)}
onImportKey={importExistingKey}
onInviteRedeemStarted={() => {
setTransitionDirection("forward");
setCurrentPage(deniedFromPage);
}}
onRetry={() => {
void saveProfileAndContinue(membershipRetryPage);
}}
Expand Down