Skip to content

Commit 577f6c6

Browse files
committed
feat: add welcome dialog and display auth flow section
1 parent 0345bfe commit 577f6c6

File tree

10 files changed

+550
-11
lines changed

10 files changed

+550
-11
lines changed

functions/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ export const authenticateLineUser = onCall(
9191
});
9292
}
9393

94-
// Return token and basic profile info to minimize additional Firestore reads
94+
console.log("isNewUser", isNewUser);
95+
9596
return {
9697
isNewUser,
9798
firebaseToken,

package-lock.json

Lines changed: 37 additions & 0 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
@@ -16,7 +16,9 @@
1616
"dependencies": {
1717
"@line/liff": "2.25.1",
1818
"@radix-ui/react-avatar": "1.1.3",
19+
"@radix-ui/react-dialog": "1.1.6",
1920
"@radix-ui/react-dropdown-menu": "2.1.6",
21+
"@radix-ui/react-slot": "1.1.2",
2022
"class-variance-authority": "0.7.1",
2123
"clsx": "2.1.1",
2224
"firebase": "11.4.0",

src/app/page.tsx

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { useState, useEffect, useRef } from "react";
34
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
45
import {
56
DropdownMenu,
@@ -8,8 +9,9 @@ import {
89
DropdownMenuSeparator,
910
DropdownMenuTrigger,
1011
} from "@/components/ui/dropdown-menu";
11-
import { LogOut } from "lucide-react";
12+
import { LogOut, Pencil } from "lucide-react";
1213
import { useAuth } from "@/contexts/AuthContext";
14+
import { WelcomeDialog } from "@/components/WelcomeDialog";
1315

1416
export default function Dashboard() {
1517
const {
@@ -19,10 +21,28 @@ export default function Dashboard() {
1921
authStatus,
2022
lineProfile,
2123
userProfile,
24+
isNewUser,
2225
} = useAuth();
26+
const [showWelcomeDialog, setShowWelcomeDialog] = useState(false);
27+
const hasShownWelcomeDialogRef = useRef(false);
2328
const profile = userProfile || lineProfile;
29+
30+
// Show welcome dialog when a new user logs in, but only once
31+
useEffect(() => {
32+
if (isNewUser && userProfile && !hasShownWelcomeDialogRef.current) {
33+
setShowWelcomeDialog(true);
34+
hasShownWelcomeDialogRef.current = true;
35+
}
36+
}, [isNewUser, userProfile]);
37+
2438
return (
2539
<div className="jun-layout jun-layout-noTransition">
40+
{/* Welcome Dialog for new users */}
41+
<WelcomeDialog
42+
open={showWelcomeDialog}
43+
onOpenChange={setShowWelcomeDialog}
44+
/>
45+
2646
<div className="jun-header jun-header-h-[64px] jun-header-clip-left px-4 md:px-4">
2747
<h1 className="font-bold text-lg">⚡️ Jun MVP Starter</h1>
2848
<div className="ml-auto">
@@ -79,9 +99,30 @@ export default function Dashboard() {
7999
<div className="px-4 py-3">
80100
<div className="font-medium">{profile.displayName}</div>
81101
{userProfile && (
82-
<div className="text-xs text-gray-500 truncate">
83-
{userProfile.providers?.line?.email || "LINE User"}
84-
</div>
102+
<>
103+
<div className="text-xs text-gray-500 truncate">
104+
{userProfile.providers?.line?.email || "LINE User"}
105+
</div>
106+
{userProfile.description ? (
107+
<button
108+
onClick={() => setShowWelcomeDialog(true)}
109+
className="mt-2 text-sm text-gray-600 italic group flex items-start w-full text-left hover:bg-gray-50 rounded px-1 py-0.5 transition-colors"
110+
>
111+
<Pencil className="h-3.5 w-3.5 mr-1 mt-0.5 text-gray-400 group-hover:text-blue-500 flex-shrink-0" />
112+
<span className="flex-1">
113+
&ldquo;{userProfile.description}&rdquo;
114+
</span>
115+
</button>
116+
) : (
117+
<button
118+
onClick={() => setShowWelcomeDialog(true)}
119+
className="mt-2 text-xs text-blue-500 hover:underline flex items-center"
120+
>
121+
<Pencil className="h-3 w-3 mr-1" />
122+
Add a description
123+
</button>
124+
)}
125+
</>
85126
)}
86127
</div>
87128
<DropdownMenuSeparator className="bg-gray-200" />
@@ -98,7 +139,7 @@ export default function Dashboard() {
98139
</div>
99140
</div>
100141
<main className="jun-content">
101-
<div className="container max-w-7xl py-8 px-4 2xl:w-full 2xl:max-w-fit 2xl:mx-[128px]">
142+
<div className="container mx-auto max-w-7xl py-8 px-4 2xl:w-full 2xl:max-w-fit 2xl:mx-[128px]">
102143
<div className="flex flex-col items-center text-center mb-12">
103144
<h2 className="text-3xl font-extrabold mb-1 bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 text-transparent bg-clip-text">
104145
Go production in minutes
@@ -107,7 +148,7 @@ export default function Dashboard() {
107148
Next.js SSG, Line Login, Firebase
108149
</p>
109150
<a
110-
href="https://github.com/siriwatknp/jun-mvp-starter/"
151+
href="https://github.com/siriwatknp/jun-stack"
111152
target="_blank"
112153
rel="noopener noreferrer"
113154
className="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-gray-100 hover:bg-gray-200 transition-colors text-sm font-bold"
@@ -159,6 +200,86 @@ export default function Dashboard() {
159200
</p>
160201
</div>
161202
</div>
203+
204+
{/* Authentication Flow Section */}
205+
<div className="mt-16">
206+
<h2 className="text-2xl font-bold text-center mb-6">
207+
Authentication Flow
208+
</h2>
209+
<div className="bg-white rounded-lg shadow-md p-6">
210+
<div className="flex flex-col space-y-4">
211+
<div className="flex items-start">
212+
<div className="flex-shrink-0 bg-blue-100 rounded-full p-2 mr-4">
213+
<span className="font-bold text-blue-600">1</span>
214+
</div>
215+
<div>
216+
<h3 className="font-medium">LIFF Initialization</h3>
217+
<p className="text-gray-600 text-sm">
218+
The app initializes the LINE LIFF SDK and checks if the
219+
user is already logged in.
220+
</p>
221+
</div>
222+
</div>
223+
224+
<div className="flex items-start">
225+
<div className="flex-shrink-0 bg-green-100 rounded-full p-2 mr-4">
226+
<span className="font-bold text-green-600">2</span>
227+
</div>
228+
<div>
229+
<h3 className="font-medium">LINE Login</h3>
230+
<p className="text-gray-600 text-sm">
231+
User clicks the login button and is redirected to
232+
LINE&apos;s login page. After successful login,
233+
they&apos;re redirected back with authentication tokens.
234+
</p>
235+
</div>
236+
</div>
237+
238+
<div className="flex items-start">
239+
<div className="flex-shrink-0 bg-purple-100 rounded-full p-2 mr-4">
240+
<span className="font-bold text-purple-600">3</span>
241+
</div>
242+
<div>
243+
<h3 className="font-medium">Firebase Integration</h3>
244+
<p className="text-gray-600 text-sm">
245+
The app sends the LINE ID token to a Firebase Cloud
246+
Function, which verifies it and creates a Firebase custom
247+
token. It also creates or updates the user&apos;s document
248+
in Firestore.
249+
</p>
250+
</div>
251+
</div>
252+
253+
<div className="flex items-start">
254+
<div className="flex-shrink-0 bg-yellow-100 rounded-full p-2 mr-4">
255+
<span className="font-bold text-yellow-600">4</span>
256+
</div>
257+
<div>
258+
<h3 className="font-medium">User Session</h3>
259+
<p className="text-gray-600 text-sm">
260+
The app signs in to Firebase using the custom token and
261+
loads the user profile data from Firestore. Authentication
262+
state is maintained using React context.
263+
</p>
264+
</div>
265+
</div>
266+
267+
<div className="flex items-start">
268+
<div className="flex-shrink-0 bg-red-100 rounded-full p-2 mr-4">
269+
<span className="font-bold text-red-600">5</span>
270+
</div>
271+
<div>
272+
<h3 className="font-medium">Logout Process</h3>
273+
<p className="text-gray-600 text-sm">
274+
When logging out, the app signs out from both Firebase and
275+
LINE, and the UI is updated to show the login button
276+
again.
277+
</p>
278+
</div>
279+
</div>
280+
</div>
281+
</div>
282+
</div>
162283
</div>
163284
</main>
164285

src/components/WelcomeDialog.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from "@/components/ui/dialog";
12+
import { Button } from "@/components/ui/button";
13+
import { Textarea } from "@/components/ui/textarea";
14+
import { updateUserData } from "@/lib/firebase";
15+
import { useAuth } from "@/contexts/AuthContext";
16+
17+
interface WelcomeDialogProps {
18+
open: boolean;
19+
onOpenChange: (open: boolean) => void;
20+
}
21+
22+
// Maximum description length
23+
const MAX_DESCRIPTION_LENGTH = 200;
24+
25+
export function WelcomeDialog({ open, onOpenChange }: WelcomeDialogProps) {
26+
const { authUser, setUserProfile, userProfile, isNewUser } = useAuth();
27+
const [description, setDescription] = useState("");
28+
const [isSubmitting, setIsSubmitting] = useState(false);
29+
const isEditing = Boolean(userProfile?.description) && !isNewUser;
30+
31+
// Reset description when dialog opens
32+
useEffect(() => {
33+
if (open) {
34+
setDescription(userProfile?.description || "");
35+
}
36+
}, [open, userProfile?.description]);
37+
38+
const handleSubmit = async () => {
39+
if (!authUser) return;
40+
41+
try {
42+
setIsSubmitting(true);
43+
44+
// Update user data in Firestore
45+
await updateUserData(authUser.uid, { description });
46+
47+
// Update local state
48+
if (userProfile) {
49+
setUserProfile({
50+
...userProfile,
51+
description,
52+
});
53+
}
54+
55+
// Close the dialog
56+
onOpenChange(false);
57+
} catch (error) {
58+
console.error("Error updating user description:", error);
59+
} finally {
60+
setIsSubmitting(false);
61+
}
62+
};
63+
64+
// Handle dialog close
65+
const handleOpenChange = (open: boolean) => {
66+
// If dialog is being closed and it's a new user with no description,
67+
// still save an empty description to prevent the dialog from showing again
68+
if (!open && isNewUser && !userProfile?.description && authUser) {
69+
updateUserData(authUser.uid, { description: "" });
70+
}
71+
72+
onOpenChange(open);
73+
};
74+
75+
// Handle description change with length limit
76+
const handleDescriptionChange = (
77+
e: React.ChangeEvent<HTMLTextAreaElement>
78+
) => {
79+
const value = e.target.value;
80+
if (value.length <= MAX_DESCRIPTION_LENGTH) {
81+
setDescription(value);
82+
}
83+
};
84+
85+
// Calculate remaining characters
86+
const remainingChars = MAX_DESCRIPTION_LENGTH - description.length;
87+
const isDescriptionChanged = userProfile?.description !== description;
88+
89+
return (
90+
<Dialog open={open} onOpenChange={handleOpenChange}>
91+
<DialogContent className="sm:max-w-[425px]">
92+
<DialogHeader>
93+
<DialogTitle>{isNewUser ? "Welcome! 👋" : "About You"}</DialogTitle>
94+
<DialogDescription>
95+
{isNewUser
96+
? "Tell us a little bit about yourself. This will be displayed in your profile."
97+
: "Update your personal description. This will be displayed in your profile."}
98+
</DialogDescription>
99+
</DialogHeader>
100+
<div className="grid gap-4 py-4">
101+
<div className="relative">
102+
<Textarea
103+
placeholder="I'm a software developer who loves hiking and photography..."
104+
value={description}
105+
onChange={handleDescriptionChange}
106+
className="min-h-[120px] resize-none pr-16"
107+
maxLength={MAX_DESCRIPTION_LENGTH}
108+
/>
109+
<div className="absolute bottom-2 right-2 text-xs text-gray-400">
110+
{remainingChars} left
111+
</div>
112+
</div>
113+
</div>
114+
<DialogFooter className="flex justify-between sm:justify-between">
115+
{isEditing && (
116+
<Button
117+
variant="outline"
118+
onClick={() => {
119+
setDescription("");
120+
}}
121+
disabled={isSubmitting}
122+
className="mr-auto"
123+
>
124+
Clear
125+
</Button>
126+
)}
127+
<Button
128+
onClick={handleSubmit}
129+
disabled={
130+
isSubmitting ||
131+
!description.trim() ||
132+
(!isDescriptionChanged && isEditing)
133+
}
134+
>
135+
{isSubmitting ? "Saving..." : "Save"}
136+
</Button>
137+
</DialogFooter>
138+
</DialogContent>
139+
</Dialog>
140+
);
141+
}

0 commit comments

Comments
 (0)