"use client"; import { redirect } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { FileWithPath, useDropzone } from "react-dropzone"; import { Icon } from "@iconify/react"; import { AES_CCM } from "@trafficlunar/asmcrypto.js"; import qrcode from "qrcode-generator"; import { MII_DECRYPTION_KEY } from "@/lib/constants"; import { nameSchema, tagsSchema } from "@/lib/schemas"; import Mii from "@/utils/mii.js/mii"; import TomodachiLifeMii from "@/utils/tomodachi-life-mii"; import TagSelector from "./submit/tag-selector"; import ImageList from "./submit/image-list"; import QrUpload from "./submit/qr-upload"; import QrScanner from "./submit/qr-scanner"; export default function SubmitForm() { const [files, setFiles] = useState([]); const handleDrop = useCallback((acceptedFiles: FileWithPath[]) => { setFiles((prev) => [...prev, ...acceptedFiles]); }, []); const { getRootProps, getInputProps } = useDropzone({ onDrop: handleDrop, maxFiles: 3, accept: { "image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp"], }, }); const [isQrScannerOpen, setIsQrScannerOpen] = useState(false); const [studioUrl, setStudioUrl] = useState(); const [generatedQrCodeUrl, setGeneratedQrCodeUrl] = useState(); const [error, setError] = useState(undefined); const [name, setName] = useState(""); const [tags, setTags] = useState([]); const [qrBytesRaw, setQrBytesRaw] = useState([]); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); // Validate before sending request const nameValidation = nameSchema.safeParse(name); if (!nameValidation.success) { setError(nameValidation.error.errors[0].message); return; } const tagsValidation = tagsSchema.safeParse(tags); if (!tagsValidation.success) { setError(tagsValidation.error.errors[0].message); return; } // Send request to server const response = await fetch("/api/submit", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, tags, qrBytesRaw }), }); const { id, error } = await response.json(); if (!response.ok) { setError(error); return; } redirect(`/mii/${id}`); }; useEffect(() => { if (qrBytesRaw.length == 0) return; const qrBytes = new Uint8Array(qrBytesRaw); const decode = async () => { setError(""); // Validate QR code size if (qrBytesRaw.length !== 372) { setError("QR code size is not a valid Tomodachi Life QR code"); return; } // Decrypt the Mii part of the QR code // (Credits to kazuki-4ys) const nonce = qrBytes.subarray(0, 8); const content = qrBytes.subarray(8, 0x70); const nonceWithZeros = new Uint8Array(12); nonceWithZeros.set(nonce, 0); let decrypted: Uint8Array = new Uint8Array(); try { decrypted = AES_CCM.decrypt(content, MII_DECRYPTION_KEY, nonceWithZeros, undefined, 16); } catch (error) { console.warn("Failed to decrypt QR code:", error); setError("Failed to decrypt QR code. It may be invalid or corrupted."); return; } const result = new Uint8Array(96); result.set(decrypted.subarray(0, 12), 0); result.set(nonce, 12); result.set(decrypted.subarray(12), 20); // Check if QR code is valid (after decryption) if (result.length !== 0x60 || (result[0x16] !== 0 && result[0x17] !== 0)) { setError("QR code is not a valid Mii QR code"); return; } // Convert to Mii classes const buffer = Buffer.from(result); const mii = new Mii(buffer); const tomodachiLifeMii = TomodachiLifeMii.fromBytes(qrBytes); if (tomodachiLifeMii.hairDyeEnabled) { mii.hairColor = tomodachiLifeMii.studioHairColor; mii.eyebrowColor = tomodachiLifeMii.studioHairColor; mii.facialHairColor = tomodachiLifeMii.studioHairColor; } try { setStudioUrl(mii.studioUrl({ width: 128 })); // Generate a new QR code for aesthetic reasons const byteString = String.fromCharCode(...qrBytes); const generatedCode = qrcode(0, "L"); generatedCode.addData(byteString, "Byte"); generatedCode.make(); setGeneratedQrCodeUrl(generatedCode.createDataURL()); } catch (error) { console.warn("Failed to get and/or generate Mii images:", error); setError("Failed to get and/or generate Mii images"); } }; decode(); }, [qrBytesRaw]); return (
{!studioUrl && Mii} Nintendo Studio URL
{!generatedQrCodeUrl && QR Code} Generated QR Code

Drag and drop your images here
or click to open

setName(e.target.value)} />
QR Code or
{error && Error: {error}}
); }