Skip to content

Commit

Permalink
feat(app): add retry connection modal if there was an error attemptin…
Browse files Browse the repository at this point in the history
…g to fetch a passport

if there was an error attempting to fetch a passport, for example if the ceramic connection drops
or timed out, it is unknown whether a user has an existing passport or needs to create a new
passport. this feature adds a modal to describe the situation and allows the user to retry the
connection if desired.

[passportxyz#196]
  • Loading branch information
shavinac committed Jun 16, 2022
1 parent 5fa2cd1 commit 7c53905
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 11 deletions.
66 changes: 65 additions & 1 deletion app/__tests__/pages/Dashboard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import Dashboard from "../../pages/Dashboard";
import { UserContext, UserContextState } from "../../context/userContext";
import { mockAddress, mockWallet } from "../../__test-fixtures__/onboardHookValues";
Expand All @@ -15,6 +15,12 @@ jest.mock("@self.id/framework", () => {
};
});

jest.mock("@self.id/web", () => {
return {
EthereumAuthProvider: jest.fn(),
};
});

const mockHandleConnection = jest.fn();
const mockCreatePassport = jest.fn();
const handleAddStamp = jest.fn();
Expand Down Expand Up @@ -194,3 +200,61 @@ describe("when viewer connection status is connecting", () => {
expect(waitingForSignature).toBeInTheDocument();
});
});

describe("when app fails to load ceramic stream", () => {
const mockUserContextUndefinedLoading = {
...mockUserContext,
passport: undefined,
isLoadingPassport: undefined,
};

it("should display a modal for user to retry connection, or close", () => {
render(
<UserContext.Provider value={mockUserContextUndefinedLoading}>
<Router>
<Dashboard />
</Router>
</UserContext.Provider>
);

const retryModal = screen.getByTestId("retry-modal-content");
expect(retryModal).toBeInTheDocument();

const retryButton = screen.getByTestId("retry-modal-try-again");
expect(retryButton).toBeInTheDocument();

const closeButton = screen.getByTestId("retry-modal-close");
expect(closeButton).toBeInTheDocument();
});

it("when retry button is clicked, it should retry ceramic connection", () => {
const mockCeramicConnect = jest.fn();
(framework.useViewerConnection as jest.Mock).mockReturnValue([{ status: "connected" }, mockCeramicConnect]);

render(
<UserContext.Provider value={mockUserContextUndefinedLoading}>
<Router>
<Dashboard />
</Router>
</UserContext.Provider>
);

fireEvent.click(screen.getByTestId("retry-modal-try-again"));

expect(mockCeramicConnect).toBeCalledTimes(1);
});

it("when done button is clicked, it should disconnect the user", () => {
render(
<UserContext.Provider value={mockUserContextUndefinedLoading}>
<Router>
<Dashboard />
</Router>
</UserContext.Provider>
);

fireEvent.click(screen.getByTestId("retry-modal-close"));

expect(mockHandleConnection).toBeCalledTimes(1);
});
});
15 changes: 8 additions & 7 deletions app/context/userContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const startingAllProvidersState: AllProvidersState = {
export interface UserContextState {
loggedIn: boolean;
passport: Passport | undefined | false;
isLoadingPassport: boolean;
isLoadingPassport: boolean | undefined;
allProvidersState: AllProvidersState;
handleCreatePassport: () => Promise<void>;
handleConnection: () => void;
Expand Down Expand Up @@ -107,7 +107,7 @@ export const UserContext = createContext(startingState);
export const UserContextProvider = ({ children }: { children: any }) => {
const [loggedIn, setLoggedIn] = useState(false);
const [passport, setPassport] = useState<Passport | undefined>(undefined);
const [isLoadingPassport, setIsLoadingPassport] = useState(true);
const [isLoadingPassport, setIsLoadingPassport] = useState<boolean | undefined>(true);
const [ceramicDatabase, setCeramicDatabase] = useState<CeramicDatabase | undefined>(undefined);
const [allProvidersState, setAllProviderState] = useState(startingAllProvidersState);

Expand Down Expand Up @@ -291,22 +291,23 @@ export const UserContextProvider = ({ children }: { children: any }) => {
};

const fetchPassport = async (database: CeramicDatabase): Promise<void> => {
setIsLoadingPassport(true);
// fetch, clean and set the new Passport state
let passport = (await database.getPassport()) as Passport;
if (passport) {
passport = cleanPassport(passport, database) as Passport;
hydrateAllProvidersState(passport);
setPassport(passport);
setIsLoadingPassport(false);
} else if (passport === false) {
handleCreatePassport();
} else {
// something is wrong with Ceramic...
datadogRum.addError("Ceramic connection failed", { address });
// no ceramic...
setAddress(undefined);
handleConnection();
setPassport(passport);
// TODO use more expressive loading states
setIsLoadingPassport(undefined);
}
setPassport(passport);
setIsLoadingPassport(false);
};

const handleCreatePassport = async (): Promise<void> => {
Expand Down
72 changes: 69 additions & 3 deletions app/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,33 @@ import { CardList } from "../components/CardList";
import { JsonOutputModal } from "../components/JsonOutputModal";

// --Chakra UI Elements
import { Spinner, useDisclosure, Alert, AlertTitle } from "@chakra-ui/react";
import {
Spinner,
useDisclosure,
Alert,
AlertTitle,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalFooter,
Button,
} from "@chakra-ui/react";

import { UserContext } from "../context/userContext";

import { useViewerConnection } from "@self.id/framework";
import { EthereumAuthProvider } from "@self.id/web";

export default function Dashboard() {
const { passport, wallet } = useContext(UserContext);
const { passport, wallet, isLoadingPassport, handleConnection } = useContext(UserContext);

const { isOpen, onOpen, onClose } = useDisclosure();

const navigate = useNavigate();

const [viewerConnection] = useViewerConnection();
const [viewerConnection, ceramicConnect] = useViewerConnection();
const { isOpen: retryModalIsOpen, onOpen: onRetryModalOpen, onClose: onRetryModalClose } = useDisclosure();

// Route user to home when wallet is disconnected
useEffect(() => {
Expand All @@ -30,6 +43,58 @@ export default function Dashboard() {
}
}, [wallet]);

// Allow user to retry Ceramic connection if failed
const retryConnection = () => {
if (isLoadingPassport == undefined && wallet) {
ceramicConnect(new EthereumAuthProvider(wallet.provider, wallet.accounts[0].address));
onRetryModalClose();
}
};

const closeModalAndDisconnect = () => {
onRetryModalClose();
// toggle wallet connect/disconnect
handleConnection();
};

// isLoadingPassport undefined when there is an issue during fetchPassport attempt
useEffect(() => {
if (isLoadingPassport == undefined) {
onRetryModalOpen();
}
}, [isLoadingPassport]);

const retryModal = (
<Modal isOpen={retryModalIsOpen} onClose={onRetryModalClose}>
<ModalOverlay />
<ModalContent>
<ModalBody mt={4}>
<div className="flex flex-row">
<div className="inline-flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-purple-100 sm:mr-10">
<img alt="shield-exclamation-icon" src="./assets/shield-exclamation-icon.svg" />
</div>
<div className="flex flex-col" data-testid="retry-modal-content">
<p className="text-lg font-bold">Unable to Connect</p>
<p>
There was an issue connecting to the Ceramic network. You can try connecting again or try again later.
</p>
</div>
</div>
</ModalBody>
{
<ModalFooter py={3}>
<Button data-testid="retry-modal-try-again" variant="outline" mr={2} onClick={retryConnection}>
Try Again
</Button>
<Button data-testid="retry-modal-close" colorScheme="purple" onClick={closeModalAndDisconnect}>
Done
</Button>
</ModalFooter>
}
</ModalContent>
</Modal>
);

return (
<>
<div className="flex w-full flex-col flex-wrap border-b-2 p-5 md:flex-row">
Expand All @@ -45,6 +110,7 @@ export default function Dashboard() {
<p className="text-xl text-black">Select the verification stamps you’d like to connect to your Passport.</p>
</div>
<div className="w-full md:w-1/4">
{isLoadingPassport == undefined && retryModal}
{viewerConnection.status === "connecting" && (
<Alert status="warning" data-testid="selfId-connection-alert">
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="orange.500" size="md" />
Expand Down
3 changes: 3 additions & 0 deletions app/public/assets/shield-exclamation-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 7c53905

Please sign in to comment.