Closed61

Next.jsとWebNFCを使って勤怠メールを送るくんを作る(PWAアドカレネタのHistory)

がっちゃんがっちゃん

はじめまして。がっちゃんです。

今回はPWAのアドベントカレンダー 10日目に登録したので、それ用のネタアプリを作っていく記録を残していきます。

自分が記事を書く予定のPWAカレンダー: https://qiita.com/advent-calendar/2020/pwa (まだまだ記事各人募集中っぽいのでよければ一緒に書きましょう)

目指すもの

WebNFCカードにPixel 3a(Android スマホ)をかざすことで、Gmail経由で勤怠メールを送信することができるWeb SPAアプリ

技術構成

実装イメージ

Chrome -> SPA(Next.js製)-> NFCカード読み取り -> HTTPリクエスト送信 -> (Webhook) -> GAS -> Gmail経由メール送信

初挑戦なもの

- Next.js
- WebNFC
 - NFCカードは持ってて、WebNFCが使われた @takepepe さんのアプリケーションをざっくり読んだことはある
- Zenn Scrap

普段使ってるもの・できること

- Nuxt(Vue) / React
- npm/yarn等
- GAS
- Figma
- JavaScriptでSPAを不自由なく作ること
がっちゃんがっちゃん

とりあえず公式を参考にCreate Next Appしとく

$ yarn create next-app web-nfc-kintai

https://nextjs.org/docs/api-reference/create-next-app

Success! Created web-nfc-kintai at /home/yuki-gatchan0807/program/web-nfc-kintai/web-nfc-kintai
Inside that directory, you can run several commands:

  yarn dev
    Starts the development server.

  yarn build
    Builds the app for production.

  yarn start
    Runs the built app in production mode.

We suggest that you begin by typing:

  cd web-nfc-kintai
  yarn dev

Done in 11.46s.

いつも使ってるNuxtと大体コマンドは一緒やね🙆

がっちゃんがっちゃん

わーい。Hello Next.js Worldできたー!

(ZennのScrap、GitHubみたいに画像をクリップボードから直接貼ることは出来なくてちょっと「あぁ…」って思ったけど、InputへのD&Dは対応してくれてて嬉しかったのでテンション的にはプラマイプラス🙆

がっちゃんがっちゃん

Hello Next World出来たので今日はおしまい!

明日はどんな感じでUIを作っていくか、手書きでカンプみたいなのを作って、その後にNext.jsのページ単位をまず作っていく予定。

がっちゃんがっちゃん

続きを1時間だけやっていき。

ページUIの手書きカンプの前にMVPとして記事にできるレベルのアプリを作るために必要な機能要件と技術要素を洗い出しする🤔

がっちゃんがっちゃん

MVPの定義

Webアプリで、WebNFCをトリガーに自分(がっちゃん)のアカウントから、メール送信ができる

MVPの機能詳細

[App]

  • NFCパスワード書き込みページ
    • メール送信をする前に、まずNFCカードに対してパスワードを書き込んでおく(NFCカードを勤怠送るくん用に登録する)ためのページ
      • NFC読み込みページでパスワード情報が登録されていない場合はこのページにリダイレクトされる
    • NFCカードのIDとNFC内に事前に書き込まれたパスワードをHashにし、GASにリクエストする
      • これによって誰でもWebHookにリクエストすることで勤怠メールを送ることが出来てしまう状態を防ぐ(WebHook URLは公開されるので、これによって認証の代わりとする)
  • NFC読み込みページ
    • ページアクセス時にWebNFC( NDEFReader / NDEFWriter )に対応しているかをチェックし、対応していない場合は対応ブラウザではない情報を表示する(合わせて、UAを元にデバイスのチェックも行う)
    • NFC読み込みを促すUIとパスワード書き込みページヘの導線を表示する
    • NFC読み込みを実行された場合、下記の処理を行う
      • NFCカードのID / 書き込まれているPWを読み込む
      • ID / PWをハッシュ値化する
      • Axios等で上記ハッシュ値をGAS Endpointに送信する
      • レスポンスを元にUIを完了/失敗を表示する

[GAS]

  • Webアプリ(Endpoint)として公開し、リクエストを受け付けて下記処理を行ってAppにレスポンスする
    • リクエストで受け取ったハッシュ値をID/PWに戻し、スプレッドシート内にあるIDとPWの組み合わせと一致するかを確認する
    • ID/PWが一致した場合、スプレッドシート内の文面テンプレートからテンプレートと文言を取得する
    • 取得した文言を元にGmailAppを通してメール送信を行う

[スプレッドシート]

  • 下記シート(簡易データベース)を用意する
    • カードのID/PWを登録するシート
    • 下記メールにかかわる3つをまとめて登録するシート
      • 文面のテンプレート
      • 選択されるタイミング
      • メールの送信先アドレス
    • テンプレートに埋め込む可変テキストを管理するシート

プロダクト価値(Value)のMVPからの拡張可能性・拡張方向性

①自分(がっちゃん)のアカウントから

  • Googleアカウントを持っているユーザーであれば誰でも
    • WebアプリからシームレスにGoogleアカウントのOAuth認証を実施し、GASの実行権限を付与->Google認証したGoogleアカウントとしてメール送信を行う
    • https://tonari-it.com/gas-gmail-sendemail (参考にできるかも?)

②メール送信

  • メール送信宛先をWebアプリから設定できる
    • CC: 対応?(これは複数Toを設定できるようにするだけでいいかも)
  • メールの文面のテンプレを登録し、直接利用できる
    • イメージとしてはNFC読み込み画面で先にテンプレを選択した上で読み取りを実施する感じ
    • もしくは、デフォルトは勤怠開始で、登録された業務開始時間を過ぎている場合はNFC読み取り時にポップアップで理由を追記できるようにするとか
      • 業務開始・終了時間の登録機能が必要
  • メールの文面のテンプレ内のテキスト(例えば遅刻理由など)を設定・入力できる
がっちゃんがっちゃん

雑な手書きUIカンプ(UIイメージ)

とりあえず自分がUIをイメージできればOKなので読みにくい(というか読めないレベルの文字の汚さな)のはご愛嬌

がっちゃんがっちゃん

ということで予定してた部分は終わったので早速明日からはNext.jsでアプリケーション作っていこう。

あと完全に忘れてたけどGAS部分の実装があるからClaspも使うし、そのリポジトリも用意しとかんとやな🤔

がっちゃんがっちゃん

まずはページを作るのと、Nuxtでいう mounted 的なタイミング( componentDidMount でいいんかな?)で、UAベースでのデバイス(ブラウザ)の確認と NDEFReader / NDEFWriter が使えるかのチェックをしよう

がっちゃんがっちゃん

あと、PCのChromeと実機をつないでリアルタイムにChrome DevToolsを見えるようにする検証の準備をせねば。

がっちゃんがっちゃん

あとそうだ。localhostのポートをポートフォワーディングしておかんとアカンのやった。

↓の「Port forwarding...」のボタンからダイアログを出して、Next.jsのDevServerを起動してるポート情報を登録して、ダイアログの下の方にある「Enable port forwarding」のチェックボックスにチェックあげると、ちょっとした後にAndroid Chrome側で localhost:XXXX でアクセスできるようになる🙆

がっちゃんがっちゃん

これで実機テスト準備OK。
Next.jsのページ作成のやり方とか調べていくぞー

がっちゃんがっちゃん

そっか。Reactベースだからexport defaultに設定したFunctionの返り値でJSX的にHTMLテキストを返すだけで良いのね。
ほとんどNext.js側でラップされてるのか🤔

あと、今までSSRが入らないページ間遷移をCSR、CSRって呼んでたけど、Client-side navigationって呼び方が正しいのか。理解理解🙆

https://nextjs.org/learn/basics/navigate-between-pages/client-side

がっちゃんがっちゃん

pages/index.js に設定された↓のあたりを読みながらふむふむしてる。

Nuxtの head() はNext.jsパッケージ内の Head コンポーネントが対応してるのね。
react-helmet的な雰囲気を感じてる

あとこれが最近噂のCSS Moduleか…!(↓のお二人の話をTLで観測して流し読みはしていた)

import Head from "next/head";
import styles from "../styles/Home.module.css";

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
/** ~~  */   
  </div>
  );
}

がっちゃんがっちゃん

これNextとTailwindCSSって組み合わせれるんかな?
多分行けるとは思うけど、、、

最近Reactアプリの方でStyled Componentいじってていいなぁと思いつつ、Nuxtアプリ2件の方ではTailwindCSSを使ってて後者の方が個人的には好みだったりするのでこっちにしたいなぁと思ってたり🤔

先にページのつなぎこみとかやってしまって、後で調べよ👀

がっちゃんがっちゃん

さて。チュートリアルのところ一通り流したのでそろそろDocsの方から目当ての機能( componentDidMount 的なライフサイクルとか、Hooks的なStateとかの扱い)を探していこう…👀

がっちゃんがっちゃん

なんか結構頭が混乱してきたんやけど、Next.jsってCSRがベースのフレームワークではない…?
そこまでは言ってない気はするものの、SSR or SSGの文脈の記事や情報が多くてCSRする場合の書き方はどうなるのかイマイチつかめていない🤔

ふつーにReactアプリのようにReact Hooksベースの書き方で良いの…か…?

がっちゃんがっちゃん

ちょっと頭こんがらがってきたのと体調まだ万全ではないので一時休戦…😪
明日早起きできたらもうちょっと調べよ。

おやすみなさい、いい夢を。

がっちゃんがっちゃん

とりあえずReact Hooksベースで NDEFReader の存在を確認する処理を追加してみる🙆

      <main className={styles.main}>
        <h1 className={styles.title}>
          カードにパスコードを<Link href="/register">登録する</Link>
        </h1>
        <NFCUsableFlag></NFCUsableFlag>
      </main>
function NFCUsableFlag() {
  const [nfcFlag, setNFCFlag] = useState("False");

  useEffect(() => {
    if (process.browser) {
      if ("NDEFReader" in window) {
        setNFCFlag("True");
      }
    }
  });

  return <div>NFC Usable: {nfcFlag}</div>;
}
がっちゃんがっちゃん

基本的なDOM構造とStyleを当ては終わったからNFCでカードの情報読み込む処理を追加するマン

がっちゃんがっちゃん

パスコード登録画面でNFCカードIDを取得するためにHooksにNFC読み込みを開始するロジック追加した🙆

  const [nfcId, setNFCId] = useState("");

  useEffect(() => {
    const reader = new NDEFReader();

    reader
      .scan()
      .then(() => {
        reader.onerror = (event) => {
          console.log(event);
          alert("何らかの原因で読み込みに失敗しました");
        };
        reader.onreading = (event) => {
          setNFCId(event.serialNumber);
        };
      })
      .catch((error) => {
        alert("NFCカードの読み込み準備に失敗しました");
      });
  });

https://github.com/gatchan0807/web-nfc-kintai/commit/077a6c5bc6e5ee303def264315cf333483840a92

がっちゃんがっちゃん

(一旦NFC関連のエラー全部握りつぶして適当なアラート出してるのはご愛嬌)

がっちゃんがっちゃん

試行錯誤中の関数

  const inputPassCode = (passcode) => {
    setPasscode(passcode);
    console.log(passcode);
    if (timer !== 0) {
      console.log(timer);
      clearTimeout(timer);
    }

    const timerId = setTimeout(() => {
      console.log("write");
      setMainMessage(MAIN_MESSAGE.WRITE_STAND_BY);
      console.log(mainMessage);
    }, 5000);
    setTimer(timerId);
  };

がっちゃんがっちゃん

useRef使わなくてよかったし何なら問題の原因、渡したPropsを描画に使ってなかったのが原因というしょうもないミスやった…

がっちゃんがっちゃん
  1. NFCカードのシリアルナンバー読み込み
  2. カード用パスコードの入力
  3. 入力されたパスコードのカードへの書き込み

までできるようになった!🙆

がっちゃんがっちゃん

書き込みチェックのために、書き込み時にオープンした NDEFWriter のセッションと一番最初にカードID確認用のためにオープンしてるセッションを閉じたいんやけどどうすればいいんやろ…🤔

がっちゃんがっちゃん

本来のAPIはClasp使ってGASに立てる予定やけど、まずローカルでチェックするためにモックAPI立てたくて、既存の /api/hello をちょちょっといじって↓な感じで簡単に立てれた🙆

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction

export default (req, res) => {
  if (req.method === "POST") {
    res.statusCode = 200;
    res.json({ name: "John Doe" });
  } else if (req.method === "GET") {
    res.statusCode = 200;
    res.json({ name: "Hello John." });
  }
};
がっちゃんがっちゃん

リクエスト部分はカードにパスコード登録する部分を流用して↓な感じに

  // 初回NFCカードID確認
  useEffect(() => {
    const reader = new NDEFReader();

    setMainMessage(MAIN_MESSAGE.READ_WAIT);
    reader
      .scan()
      .then(() => {
        reader.onerror = (event) => {
          console.log(event);
          alert("何らかの原因で読み込みに失敗しました");
        };
        reader.onreading = async (event) => {
          console.log(event.serialNumber);
          setMainMessage(MAIN_MESSAGE.SENDING);
          const res = await fetch("/api/hello");
          console.log(await res.json());
        };
      })
}, [])
がっちゃんがっちゃん

APIリクエスト部分はちょっとコード量多くなりそうやし別の関数に切り出そ

がっちゃんがっちゃん

ってかそうか。NFCの reader.scan のハンドラーたちもこれ、関数にくくりだして共通化できるな🤔

がっちゃんがっちゃん

とりあえずリファクタをして、 共通のHandlerは一旦 lib/nfcCommonHandler.js ってファイル作ってそこにまとめた🙆

Read系のページとタイミングによって異なる処理のハンドラはUI返すコンポーネントの下にプライベートな関数として定義して対応。

多分これ useEffect とかCustom Hooksとかもっとちゃんと使いこなせたらもっときれいに書けるんやろけどいまいちそこを理解してない状態で使うのはドツボにはまるからスキップ…!

https://ja.reactjs.org/docs/hooks-custom.html

がっちゃんがっちゃん

Next.js側はAPIにリクエストする処理を書けばOKで、今度はそのリクエストを受け取るAPIをGAS上に作るところを進めねば

このスクラップは2023/03/01にクローズされました