-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into create-my-content
- Loading branch information
Showing
21 changed files
with
861 additions
and
457 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import { CloudTasksClient } from "@google-cloud/tasks"; | ||
import * as admin from "firebase-admin"; | ||
import * as logger from "firebase-functions/logger"; | ||
import { | ||
onDocumentCreated, | ||
onDocumentDeleted, | ||
} from "firebase-functions/v2/firestore"; | ||
import { GoogleAuth } from "google-auth-library"; | ||
|
||
const tasksClient = new CloudTasksClient(); | ||
const projectId = process.env.GCP_PROJECT_ID; | ||
const region = "asia-northeast1"; | ||
const queue = "deadline-notification-queue"; | ||
|
||
export const createTasksOnGoalCreate = onDocumentCreated( | ||
{ region: region, document: "goal/{goalId}" }, | ||
async (event) => { | ||
// production以外はスキップ(Cloud Tasksがエミュレーターで実行できないため) | ||
if (process.env.NODE_ENV !== "production") { | ||
return; | ||
} | ||
|
||
if (!projectId) { | ||
logger.info("GCP_PROJECT_ID is not defined."); | ||
return; | ||
} | ||
|
||
if (!event.data) { | ||
logger.info("No data found in event."); | ||
return; | ||
} | ||
|
||
try { | ||
const goalData = event.data.data(); | ||
const marginTime = 2; | ||
// 期限のmarginTime分前にタスクを設定 | ||
const deadline = new Date( | ||
goalData.deadline.toDate().getTime() - marginTime * 60 * 1000 | ||
); | ||
const goalId = event.params.goalId; | ||
const fcmToken = await getUserFcmToken(goalData.userId); | ||
const postData = { | ||
message: { | ||
token: fcmToken, // 通知を受信する端末のトークン | ||
notification: { | ||
title: `${marginTime}分以内に目標を完了し写真をアップロードしましょう!`, | ||
body: goalData.text, | ||
}, | ||
}, | ||
}; | ||
const queuePath = tasksClient.queuePath(projectId, region, queue); | ||
const auth = new GoogleAuth({ | ||
scopes: ["https://www.googleapis.com/auth/cloud-platform"], | ||
}); | ||
const accessToken = await auth.getAccessToken(); | ||
|
||
await tasksClient.createTask({ | ||
parent: queuePath, | ||
task: { | ||
name: `${queuePath}/tasks/${goalId}`, // タスクの名前をgoalIdに設定 | ||
httpRequest: { | ||
httpMethod: "POST", | ||
url: `https://fcm.googleapis.com//v1/projects/${projectId}/messages:send`, | ||
headers: { | ||
"Content-Type": "application/json", | ||
Authorization: `Bearer ${accessToken}`, | ||
}, | ||
body: Buffer.from(JSON.stringify(postData)).toString("base64"), | ||
}, | ||
scheduleTime: { seconds: Math.floor(deadline.getTime() / 1000) }, | ||
}, | ||
}); | ||
logger.info("Task created for goalId:", goalId); | ||
} catch (error) { | ||
logger.info("Error scheduling task:", error); | ||
} | ||
} | ||
); | ||
|
||
export const deleteTasksOnGoalDelete = onDocumentDeleted( | ||
{ region: region, document: "goal/{goalId}" }, | ||
async (event) => { | ||
if (process.env.NODE_ENV !== "production") { | ||
return; | ||
} | ||
|
||
if (!projectId) { | ||
logger.info("GCP_PROJECT_ID is not defined."); | ||
return; | ||
} | ||
|
||
if (!event.data) { | ||
logger.info("No data found in event."); | ||
return; | ||
} | ||
|
||
try { | ||
const goalId = event.params.goalId; | ||
const queuePath = tasksClient.queuePath(projectId, region, queue); | ||
const taskName = `${queuePath}/tasks/${goalId}`; // goalIdが名前になっているタスクを削除 | ||
await tasksClient.deleteTask({ name: taskName }); | ||
logger.info("Task deleted for goalId:", goalId); | ||
} catch (error) { | ||
logger.info("Error deleting task:", error); | ||
} | ||
} | ||
); | ||
|
||
export const deleteTasksOnPostCreate = onDocumentCreated( | ||
{ | ||
region: region, | ||
document: "post/{postId}", | ||
}, | ||
async (event) => { | ||
if (process.env.NODE_ENV !== "production") { | ||
return; | ||
} | ||
|
||
if (!projectId) { | ||
logger.info("GCP_PROJECT_ID is not defined."); | ||
return; | ||
} | ||
|
||
if (!event.data) { | ||
logger.info("No data found in event."); | ||
return; | ||
} | ||
|
||
try { | ||
const postData = event.data.data(); | ||
const goalId = postData.goalId; | ||
const queuePath = tasksClient.queuePath(projectId, region, queue); | ||
const taskName = `${queuePath}/tasks/${goalId}`; // goalIdが名前になっているタスクを削除 | ||
await tasksClient.deleteTask({ name: taskName }); | ||
logger.info("Task deleted for goalId:", goalId); | ||
} catch (error) { | ||
logger.info("Error deleting task:", error); | ||
} | ||
} | ||
); | ||
|
||
const getUserFcmToken = async (userId: string) => { | ||
const userData = await admin | ||
.firestore() | ||
.collection("user") | ||
.doc(userId) | ||
.get() | ||
.then((doc) => doc.data()); | ||
if (!userData) { | ||
throw new Error(`No user data found for userId:, ${userId}`); | ||
} | ||
if (!userData.fcmToken) { | ||
throw new Error(`No FCM token found for userId:, ${userId}`); | ||
} | ||
return userData.fcmToken; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
"use client"; | ||
import { appCheckToken, auth, functionsEndpoint } from "@/app/firebase"; | ||
import { useUser } from "@/utils/UserContext"; | ||
import { | ||
DialogContent, | ||
DialogTitle, | ||
Input, | ||
Modal, | ||
ModalDialog, | ||
} from "@mui/joy"; | ||
import Button from "@mui/material/Button"; | ||
import Stack from "@mui/material/Stack"; | ||
import { updateProfile } from "firebase/auth"; | ||
import React, { useState } from "react"; | ||
|
||
export default function NameUpdate({ | ||
open, | ||
setOpen, | ||
}: { | ||
open: boolean; | ||
setOpen: (open: boolean) => void; | ||
}) { | ||
const [newName, setNewName] = useState(""); | ||
const { user } = useUser(); | ||
const handleNameUpdate = async (event: React.FormEvent) => { | ||
event.preventDefault(); | ||
|
||
const response = await fetch(`${functionsEndpoint}/user/${user?.uid}`, { | ||
method: "PUT", | ||
headers: { | ||
"X-Firebase-AppCheck": appCheckToken, | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ name: newName }), | ||
}); | ||
|
||
// Firebase Authentication の displayName を更新 | ||
if (auth.currentUser) { | ||
await updateProfile(auth.currentUser, { displayName: newName }); | ||
console.log("Firebase displayName updated successfully"); | ||
} else { | ||
console.error("Failed to update displayName: No authenticated user"); | ||
} | ||
|
||
if (!response.ok) { | ||
console.error("Failed to update name"); | ||
} else { | ||
console.log("Name updated successfully"); | ||
setNewName(""); | ||
setOpen(false); | ||
} | ||
}; | ||
|
||
// 以下のJoy UIによるエラーを無効化 | ||
// Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release. Error Component Stack | ||
try { | ||
const consoleError = console.error; | ||
console.error = (...args) => { | ||
if (args[0]?.includes("Accessing element.ref was removed")) { | ||
return; | ||
} | ||
consoleError(...args); | ||
}; | ||
} catch { | ||
console.error("Failed to disable Joy UI error"); | ||
} | ||
|
||
return ( | ||
<Modal open={open} onClose={() => setOpen(false)} keepMounted disablePortal> | ||
<ModalDialog | ||
aria-labelledby="update-name-title" | ||
aria-describedby="update-name-description" | ||
> | ||
<DialogTitle id="update-name-title">名前を変更</DialogTitle> | ||
<DialogContent id="update-name-description"> | ||
新しい名前を入力してください. | ||
</DialogContent> | ||
<form onSubmit={handleNameUpdate}> | ||
<Stack spacing={2} sx={{ mt: 2 }}> | ||
<Input | ||
placeholder="New Name" | ||
value={newName} | ||
onChange={(e) => setNewName(e.target.value)} | ||
required | ||
/> | ||
<Stack direction="row" spacing={1} justifyContent="flex-end"> | ||
<Button | ||
variant="outlined" | ||
color="primary" | ||
onClick={() => setOpen(false)} | ||
> | ||
Cancel | ||
</Button> | ||
<Button type="submit" variant="contained" color="primary"> | ||
Update Name | ||
</Button> | ||
</Stack> | ||
</Stack> | ||
</form> | ||
</ModalDialog> | ||
</Modal> | ||
); | ||
} |
Oops, something went wrong.