Skip to content

Commit d52eb36

Browse files
authored
Merge pull request #40 from aws-samples/lesang/add-token-join
Adds the ability to join a stage using a participant token
2 parents 25c402e + 1ec8cd0 commit d52eb36

24 files changed

+765
-167
lines changed

.pre-commit-config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
repos:
2+
- repo: https://github.com/semgrep/pre-commit
3+
rev: "v1.122.0"
4+
hooks:
5+
- id: semgrep
6+
entry: semgrep
7+
args: ["--config", "p/react", "--error", "--skip-unknown-extensions"]
8+
- repo: local
9+
hooks:
10+
- id: trufflehog
11+
name: TruffleHog
12+
description: Detect secrets in your data.
13+
entry: bash -c 'trufflehog git file://. --since-commit HEAD --results=verified,unknown --fail --no-update'
14+
language: system
15+
stages: ["pre-commit", "pre-push"]

package-lock.json

Lines changed: 24 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"dependencies": {
1313
"@aws-sdk/client-cloudformation": "^3.624.0",
1414
"@aws-sdk/credential-providers": "^3.624.0",
15+
"@custom-react-hooks/use-clipboard": "^1.6.0",
1516
"@generouted/react-router": "^1.19.5",
1617
"@heroicons/react": "^2.1.3",
1718
"@react-three/fiber": "^9.4.0",
@@ -22,6 +23,7 @@
2223
"boring-avatars": "^2.0.4",
2324
"classnames": "^2.5.1",
2425
"clsx": "^2.1.1",
26+
"jwt-decode": "^4.0.0",
2527
"motion": "^12.23.24",
2628
"react": "19.2.0",
2729
"react-dom": "19.2.0",

src/components/JoinSessionDialog.jsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useContext, useEffect } from 'react';
2-
import { ModalContext } from '../contexts/ModalContext';
3-
import { AnimatedModal } from './AnimatedModal';
4-
import { JoinSession } from './JoinSession';
1+
import { useContext, useEffect } from "react";
2+
import { ModalContext } from "../contexts/ModalContext";
3+
import { AnimatedModal } from "./AnimatedModal";
4+
import { JoinSession } from "./JoinSession";
55

66
export function JoinSessionDialog({
77
username,
@@ -21,7 +21,7 @@ export function JoinSessionDialog({
2121
}, []);
2222

2323
return (
24-
<main className='w-[100dvw] h-[100dvh] bg-surface dark:bg-surfaceAlt'>
24+
<main className="w-[100dvw] h-[100dvh] bg-surface dark:bg-surfaceAlt">
2525
<AnimatedModal isOpen={modalOpen} onRequestClose={() => null}>
2626
<JoinSession
2727
username={username}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { useState } from "react";
2+
import { useClipboard } from "@custom-react-hooks/use-clipboard";
3+
import { createSessionWithToken } from "../sdk/Session";
4+
import { decodeJWT, isJWTExpired } from "../helpers/jwt";
5+
import { Button } from "./Buttons";
6+
import { TokenInput } from "./TokenInput";
7+
import useToast from "../hooks/useToast";
8+
import { ArrowUpRightIcon } from "@heroicons/react/20/solid";
9+
10+
export function JoinSessionWithToken({ handleSuccess }) {
11+
const [token, setToken] = useState("");
12+
const [loading, setLoading] = useState(false);
13+
const [error, setError] = useState(undefined);
14+
const { showToast } = useToast();
15+
const { pasteFromClipboard } = useClipboard();
16+
17+
async function handleSubmit(e) {
18+
e.preventDefault();
19+
try {
20+
setLoading(true);
21+
setError(undefined);
22+
23+
// Decode the token
24+
const decodedToken = decodeJWT(token);
25+
26+
// Check if token is not expired
27+
if (isJWTExpired(token)) {
28+
throw new Error("Token has expired");
29+
}
30+
31+
const { sessionId, attributes, expiration, hasPublish } =
32+
createSessionWithToken(decodedToken);
33+
34+
handleSuccess({ sessionId, token, attributes, expiration, hasPublish });
35+
} catch (err) {
36+
const errorMessage = err.message || "Could not join session";
37+
showToast(errorMessage, "ERROR", "join-error-toast");
38+
setError(errorMessage);
39+
setLoading(false);
40+
}
41+
}
42+
43+
const updateToken = (e) => {
44+
setToken(e.target.value);
45+
setError(undefined);
46+
};
47+
48+
async function handlePasteToken() {
49+
try {
50+
const pastedText = await pasteFromClipboard();
51+
if (pastedText && pastedText.trim()) {
52+
setToken(pastedText.trim());
53+
setError(undefined);
54+
showToast(
55+
"Token pasted from clipboard",
56+
"SUCCESS",
57+
"paste-success-toast"
58+
);
59+
} else {
60+
showToast("Clipboard is empty", "ERROR", "paste-error-toast");
61+
}
62+
} catch (err) {
63+
const errorMessage =
64+
"Failed to paste from clipboard. Please check clipboard permissions.";
65+
showToast(errorMessage, "ERROR", "paste-error-toast");
66+
setError(errorMessage);
67+
}
68+
}
69+
70+
return (
71+
<div className="bg-surface w-96 px-6 py-8 rounded-xl overflow-hidden flex flex-col gap-2 text-uiText/50 shadow-xl dark:shadow-black/80 ring-1 ring-surfaceAlt2/10">
72+
<h3 id="title" className="text-md font-bold text-uiText text-center">
73+
Enter your participant token
74+
</h3>
75+
<span id="full_description" className="hidden">
76+
<p>Enter your participant token to continue.</p>
77+
</span>
78+
<span className="text-xs text-center text-pretty mb-4 text-uiTextAlt2">
79+
You can create a token through the AWS&nbsp;console or AWS&nbsp;CLI.{" "}
80+
<a
81+
href="https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/getting-started-distribute-tokens.html#getting-started-distribute-tokens-console"
82+
target="_blank"
83+
rel="noopener noreferrer"
84+
className="group relative inline-flex gap-x-0 items-center text-secondary before:content-[''] before:absolute before:-inset-y-0.5 before:-inset-x-1 hover:before:bg-surfaceAlt2/5 before:rounded"
85+
>
86+
Learn more
87+
<span className="overflow-hidden">
88+
<ArrowUpRightIcon className="size-4 group-hover:animate-arrow-hover" />
89+
</span>
90+
</a>
91+
</span>
92+
<form onSubmit={handleSubmit}>
93+
<div className="flex justify-center gap-x-2 mb-5">
94+
<TokenInput
95+
placeholder="eyJhb..."
96+
inputValue={token}
97+
onChange={updateToken}
98+
error={error}
99+
/>
100+
</div>
101+
<div className="flex flex-col gap-y-3">
102+
<Button
103+
appearance="default"
104+
style="roundedText"
105+
fullWidth={true}
106+
type="button"
107+
onClick={handlePasteToken}
108+
>
109+
Paste
110+
</Button>
111+
<Button
112+
appearance="primary"
113+
style="roundedText"
114+
fullWidth={true}
115+
loading={loading}
116+
disabled={token?.length < 6}
117+
type="submit"
118+
>
119+
Join session
120+
</Button>
121+
</div>
122+
</form>
123+
</div>
124+
);
125+
}

src/components/LocalParticipant.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ const LocalParticipant = function LocalParticipant({ tooltipId }) {
1111

1212
const audioMuted = stageParticipant?.audioMuted || false;
1313
const videoStopped = currentVideoDevice?.isMuted || false;
14-
const userName = stageParticipant?.attributes?.username || "undefined";
14+
const userName = stageParticipant?.attributes?.username;
1515

1616
return (
1717
<Participant
1818
id={"You"}
19-
userId={"You"}
19+
userId={stageParticipant?.userId}
2020
userName={userName}
2121
isLocal={true}
2222
tooltipId={tooltipId}

0 commit comments

Comments
 (0)