Skip to content

Commit

Permalink
fast one click (#1907)
Browse files Browse the repository at this point in the history
  • Loading branch information
ichub authored Sep 27, 2024
1 parent 8062504 commit 15e25e5
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { requestGenericIssuanceTicketPreviews } from "@pcd/passport-interface";
import { Button, Spacer } from "@pcd/passport-ui";
import { IPODTicketData } from "@pcd/pod-ticket-pcd";
import { PODTicketCardBodyImpl } from "@pcd/pod-ticket-pcd-ui";
import { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { appConfig } from "../../../src/appConfig";
import { useDispatch, useSelf } from "../../../src/appHooks";
import { MaybeModal } from "../../modals/Modal";
import { AppContainer } from "../../shared/AppContainer";
import {
CardBodyContainer,
CardHeader,
CardOutlineExpanded
} from "../../shared/PCDCard";
import { ScreenLoader } from "../../shared/ScreenLoader";

/**
* format: http://localhost:3000/#/one-click-login/:email/:code/:targetFolder/:pipelineId?/:serverUrl?
* - `code` is the pretix or lemonade order code
* - `email` is the email address of the ticket to whom the ticket was issued
* - `targetFolder` is the folder to redirect to after login. optional.
* example: http://localhost:3000/#/one-click-login/[email protected]/123456/0xPARC%2520Summer%2520'24
*/
export function NewOneClickLoginScreen(): JSX.Element | null {
const self = useSelf();
const dispatch = useDispatch();
const { email, code, targetFolder, pipelineId, serverUrl } = useParams();
const [ticketPreviews, setTicketPreviews] = useState<IPODTicketData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | undefined>();

const redirectToTargetFolder = useCallback(() => {
if (targetFolder) {
window.location.hash = `#/?folder=${encodeURIComponent(targetFolder)}`;
} else {
window.location.hash = "#/";
}
}, [targetFolder]);

const handleLoadTicketPreviews = useCallback(async () => {
if (!email || !code) {
return;
}
try {
const previewRes = await requestGenericIssuanceTicketPreviews(
serverUrl ?? appConfig.zupassServer,
email,
code,
pipelineId
);

setLoading(false);
if (previewRes.success) {
setTicketPreviews(previewRes.value.tickets);
} else {
setError(previewRes.error);
}
} catch (err) {
await dispatch({
type: "error",
error: {
title: "An error occured",
message: (err as Error).message || "An error occured"
}
});
}
}, [email, code, serverUrl, pipelineId, dispatch]);

const handleOneClickLogin = useCallback(async () => {
if (!email || !code) {
return;
}

try {
setLoading(true);
await dispatch({
type: "one-click-login",
email,
code,
targetFolder
});
} catch (err) {
setLoading(false);
await dispatch({
type: "error",
error: {
title: "An error occured",
message: (err as Error).message || "An error occured"
}
});
}
}, [email, code, dispatch, targetFolder]);

useEffect(() => {
if (process.env.ONE_CLICK_LOGIN_ENABLED !== "true") {
window.location.hash = "#/";
return;
}

if (self) {
if (!self.emails?.includes(email as string)) {
alert(
`You are already logged in as ${
self.emails.length === 1
? self.emails?.[0]
: "an account that owns the following email addresses: " +
self.emails.join(", ")
}. Please log out and try navigating to the link again.`
);
window.location.hash = "#/";
} else {
redirectToTargetFolder();
}
} else if (!email || !code) {
window.location.hash = "#/";
} else {
handleLoadTicketPreviews();
}
}, [
self,
targetFolder,
handleLoadTicketPreviews,
redirectToTargetFolder,
email,
code
]);

return (
<>
<MaybeModal fullScreen />
<AppContainer bg="primary">
{loading && <ScreenLoader />}

{error && <div>{error}</div>}

{!loading && (
<>
<Spacer h={32} />

{ticketPreviews.length === 0 ? (
<div>No tickets found</div>
) : (
<>
{ticketPreviews.map((ticket, i) => (
<CardOutlineExpanded key={i}>
<CardBodyContainer>
<CardHeader isMainIdentity={true}>
{ticket.eventName} ({ticket.ticketName})
</CardHeader>
<PODTicketCardBodyImpl
idBasedVerifyURL=""
ticketData={ticket}
key={ticket.ticketId}
/>
</CardBodyContainer>
</CardOutlineExpanded>
))}
<Spacer h={16} />
</>
)}

<Button onClick={handleOneClickLogin}>Continue to Zupass</Button>
</>
)}
</AppContainer>
</>
);
}
5 changes: 5 additions & 0 deletions apps/passport-client/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { AlreadyRegisteredScreen } from "../components/screens/LoginScreens/Alre
import { CreatePasswordScreen } from "../components/screens/LoginScreens/CreatePasswordScreen";
import { LoginInterstitialScreen } from "../components/screens/LoginScreens/LoginInterstitialScreen";
import { LoginScreen } from "../components/screens/LoginScreens/LoginScreen";
import { NewOneClickLoginScreen } from "../components/screens/LoginScreens/NewOneClickLoginScreen";
import { NewPassportScreen } from "../components/screens/LoginScreens/NewPassportScreen";
import { OneClickLoginScreen } from "../components/screens/LoginScreens/OneClickLoginScreen";
import { PrivacyNoticeScreen } from "../components/screens/LoginScreens/PrivacyNoticeScreen";
Expand Down Expand Up @@ -150,6 +151,10 @@ function RouterImpl(): JSX.Element {
path="one-click-login/:email/:code/:targetFolder"
element={<OneClickLoginScreen />}
/>
<Route
path="one-click-preview/:email/:code/:targetFolder/:pipelineId?/:serverUrl?"
element={<NewOneClickLoginScreen />}
/>
<Route
path="enter-confirmation-code"
element={<EnterConfirmationCodeScreen />}
Expand Down
22 changes: 21 additions & 1 deletion apps/passport-server/src/routing/routes/genericIssuanceRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
PodboxTicketActionRequest,
PodboxTicketActionResponseValue,
PollFeedRequest,
PollFeedResponseValue
PollFeedResponseValue,
TicketPreviewResultValue
} from "@pcd/passport-interface";
import { SerializedSemaphoreGroup } from "@pcd/semaphore-group-pcd";
import { sleep } from "@pcd/util";
Expand Down Expand Up @@ -667,4 +668,23 @@ export function initGenericIssuanceRoutes(
res.json(result satisfies GenericIssuanceSendPipelineEmailResponseValue);
}
);

app.get(
"/generic-issuance/api/ticket-previews/:email/:orderCode/:pipelineId?",
async (req, res) => {
checkGenericIssuanceServiceStarted(genericIssuanceService);

const email = checkUrlParam(req, "email");
const orderCode = checkUrlParam(req, "orderCode");
const pipelineId = req.params.pipelineId;

const result = await genericIssuanceService.handleGetTicketPreview(
email,
orderCode,
pipelineId
);

res.json(result satisfies TicketPreviewResultValue);
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
PodboxTicketActionRequest,
PodboxTicketActionResponseValue,
PollFeedRequest,
PollFeedResponseValue
PollFeedResponseValue,
TicketPreviewResultValue
} from "@pcd/passport-interface";
import { RollbarService } from "@pcd/server-shared";
import { Request } from "express";
Expand Down Expand Up @@ -55,13 +56,15 @@ import {
IBadgeGiftingDB,
IContactSharingDB
} from "../../database/queries/ticketActionDBs";
import { PCDHTTPError } from "../../routing/pcdHttpError";
import { ApplicationContext } from "../../types";
import { logger } from "../../util/logger";
import { DiscordService } from "../discordService";
import { EmailService } from "../emailService";
import { PagerDutyService } from "../pagerDutyService";
import { PersistentCacheService } from "../persistentCacheService";
import { InMemoryPipelineAtomDB } from "./InMemoryPipelineAtomDB";
import { PretixPipeline } from "./pipelines/PretixPipeline";
import { Pipeline, PipelineUser } from "./pipelines/types";
import { CredentialSubservice } from "./subservices/CredentialSubservice";
import { PipelineSubservice } from "./subservices/PipelineSubservice";
Expand Down Expand Up @@ -345,4 +348,43 @@ export class GenericIssuanceService {
public async getEdgeCityBalances(): Promise<EdgeCityBalance[]> {
return getEdgeCityBalances(this.context.dbPool);
}

/**
* Given an email and order code, and optionally a pipeline ID (which defaults to DEVCON_PIPELINE_ID),
* returns the ticket previews for the given email and order code. A ticket preview is basically a
* PODTicket in non-pcd form - just the raw IPODTicketData. This is used to display all the tickets
* a user might need when trying to check into an event, without having them go through a costly
* and slow account registration flow.
*/
public async handleGetTicketPreview(
email: string,
orderCode: string,
pipelineId?: string
): Promise<TicketPreviewResultValue> {
const requestedPipelineId = pipelineId ?? process.env.DEVCON_PIPELINE_ID;
const pipeline = (await this.getAllPipelineInstances()).find(
(p) => p.id === requestedPipelineId && PretixPipeline.is(p)
) as PretixPipeline | undefined;

if (!pipeline) {
throw new PCDHTTPError(
400,
"handleGetTicketPreview: pipeline not found " + requestedPipelineId
);
}

const tickets = await pipeline.getAllTickets();

const matchingTickets = tickets.atoms.filter(
(atom) => atom.email === email && atom.orderCode === orderCode
);

const ticketDatas = matchingTickets.map(
(atom) => pipeline.atomToPODTicketData(atom, "1") // fake semaphore id as it's not needed for the ticket preview
);

return {
tickets: ticketDatas
} satisfies TicketPreviewResultValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,7 @@ export class PretixPipeline implements BasePipeline {
});
}

private atomToPODTicketData(
public atomToPODTicketData(
atom: PretixAtom,
semaphoreV4Id: string
): IPODTicketData {
Expand Down
1 change: 1 addition & 0 deletions packages/lib/passport-interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@pcd/pcd-collection": "0.11.6",
"@pcd/pcd-types": "0.11.4",
"@pcd/pod": "0.1.7",
"@pcd/pod-ticket-pcd": "0.1.7",
"@pcd/semaphore-group-pcd": "0.11.6",
"@pcd/semaphore-identity-pcd": "0.11.6",
"@pcd/pod-pcd": "0.1.7",
Expand Down
5 changes: 5 additions & 0 deletions packages/lib/passport-interface/src/RequestTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { EdDSAPublicKey } from "@pcd/eddsa-pcd";
import { EdDSATicketPCD, EdDSATicketPCDTypeName } from "@pcd/eddsa-ticket-pcd";
import { PCDAction } from "@pcd/pcd-collection";
import { ArgsOf, PCDOf, PCDPackage, SerializedPCD } from "@pcd/pcd-types";
import { IPODTicketData } from "@pcd/pod-ticket-pcd/src/schema";
import { SerializedSemaphoreGroup } from "@pcd/semaphore-group-pcd";
import { SemaphoreSignaturePCD } from "@pcd/semaphore-signature-pcd";
import { Credential } from "./Credential";
Expand Down Expand Up @@ -1236,3 +1237,7 @@ export type OneClickEmailResponseValue = {
*/
values: Record<string, string[]>;
};

export type TicketPreviewResultValue = {
tickets: Array<IPODTicketData>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import urlJoin from "url-join";
import { TicketPreviewResultValue } from "../RequestTypes";
import { APIResult } from "./apiResult";
import { httpGetSimple } from "./makeRequest";

/**
* Asks the server to fetch the ticket previews for the given email and order code.
*/
export async function requestGenericIssuanceTicketPreviews(
zupassServerUrl: string,
email: string,
orderCode: string,
pipelineId?: string
): Promise<GenericIssuanceTicketPreviewResponse> {
return httpGetSimple(
urlJoin(
zupassServerUrl,
`/generic-issuance/api/ticket-previews`,
encodeURIComponent(email),
encodeURIComponent(orderCode)
) + (pipelineId ? `/${pipelineId}` : ""),
async (resText) => ({
value: JSON.parse(resText) as TicketPreviewResultValue,
success: true
}),
{}
);
}

export type GenericIssuanceTicketPreviewResponse =
APIResult<TicketPreviewResultValue>;
1 change: 1 addition & 0 deletions packages/lib/passport-interface/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export * from "./api/requestGenericIssuanceSemaphoreGroup";
export * from "./api/requestGenericIssuanceSemaphoreGroupRoot";
export * from "./api/requestGenericIssuanceSendPipelineEmail";
export * from "./api/requestGenericIssuanceSetManualCheckInState";
export * from "./api/requestGenericIssuanceTicketPreviews";
export * from "./api/requestGenericIssuanceUpsertPipeline";
export * from "./api/requestGenericIssuanceValidSemaphoreGroup";
export * from "./api/requestIssuanceServiceEnabled";
Expand Down
3 changes: 3 additions & 0 deletions packages/lib/passport-interface/tsconfig.cjs.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
{
"path": "../pod/tsconfig.cjs.json"
},
{
"path": "../../pcd/pod-ticket-pcd/tsconfig.cjs.json"
},
{
"path": "../../pcd/semaphore-group-pcd/tsconfig.cjs.json"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/lib/passport-interface/tsconfig.esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
{
"path": "../pod/tsconfig.esm.json"
},
{
"path": "../../pcd/pod-ticket-pcd/tsconfig.esm.json"
},
{
"path": "../../pcd/semaphore-group-pcd/tsconfig.esm.json"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/lib/passport-interface/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
{
"path": "../pod"
},
{
"path": "../../pcd/pod-ticket-pcd"
},
{
"path": "../../pcd/semaphore-group-pcd"
},
Expand Down
1 change: 1 addition & 0 deletions packages/pcd/pod-ticket-pcd/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./PODTicketPCD";
export * from "./PODTicketPCDPackage";
export * from "./schema";
Loading

0 comments on commit 15e25e5

Please sign in to comment.