はじめに
とあるイベントの運営に関わらせてもらって、スタンプカードがほしいなー となりました。チェックポイントでQRコードを読んだら、スタンプが押されていく仕組み。
こんなのが、QRを読んだ後に出てくればよし。(↑は最終的にできたシステム)
動作の確認はこちら。
ウェブサービスを調べると、有料だったり、過剰だったり、ログインとかユーザー登録とかさせたくないし。そんなヘビーなものを求めてないんですよね。
考えてみたら、静的HTMLでも簡単にできる んじゃね?ということで、やってみたらできましたの話。欲する人は多いけど、エンジニア的には簡単、というおいしい話です。
ソースはこちらにあります。
要件の整理
- スタンプは、特定のURLへアクセスする(QRコードを読み取る)ことで押すことになる
- スタンプの設置台ごとのURLは、
card.html?stampId=stamp1
など - 設置台ごとにIDが異なり、押されるスタンプの絵も異なる
- ユーザーは、自分でスタンプの状態を確認できる
- イベントは1日だけで、小規模なものなので、ズルをしてたくさんスタンプを押す対策などは、あんまり考えなくてよい
方法概要
- クッキーを使って、ユーザー(のスマホ)を識別する
- 同様にクッキーを使って、スタンプを押した状態を保持する
- URLパラメータからスタンプ台を認識して、どのスタンプを押したかを識別する
いや、大したことなさすぎて震えます。
詳細
私はReactでやったのでReactで説明しますが、素のJavaScriptでもできちゃう、むしろその方がいいまであるレベルです。
本当に細かいところは、githubをご覧ください。
state
ユーザーIDとスタンプID配列をuseState
で定義します。初期値は、クッキーから取得。初回アクセス時は、ユーザーIDを生成したいので、getOrCreateです。
const [userId, setUserId] = useState<string>(
getOrCreateUniqueUserId()
);
const [stampIds, setStampIds] = useState<string[]>(
getCookieValues("stamp_ids")
);
一応getOrCreateUniqueUserId()
とgetCookieValues()
も載せておきます。
// クッキーを設定する
export const setCookie = (key:string, value:string) => {
document.cookie = `${key}=${value}; ` +
`path=/; max-age=${COOKIE_EXPIRATION_TIME}`;
}
// クッキーからユーザーIDを取得する or 作成する
export const getOrCreateUniqueUserId = ():string => {
const key = 'user_id';
const match = document.cookie.match(new RegExp(`${key}=([^;]+)`));
if (match) return match[1];
const id = crypto.randomUUID();
// 1日だけ有効なクッキーを作成する
setCookie(key, id);
return id;
}
// クッキーから文字列の配列を取得する
export const getCookieValues = (key: string):string[] => {
const match = document.cookie.match(new RegExp(`${key}=([^;]+)`));
if (match) return match[1].split(',');
return [];
}
新しいスタンプが押された処理
URLパラメータとして、/?stampId=stamp1
などとstampId
が渡ってくるので、これをuseEffect()
で受け取って、stateのstampIds
を更新します。
// 新しいスタンプを、stampIdsに追加し、クッキーを更新する
const addNewStamp = (newStampId:string) => {
// 今回のスタンプIDが、存在していなければ追加する
if (!stampIds.includes(newStampId)) {
const newStampIds = [...stampIds, newStampId];
// state値を更新
setStampIds(newStampIds);
// クッキーを更新
setCookie('stamp_ids', newStampIds.join(','));
}
}
useEffect(() => {
// URLパラメータからstampIdを取得
const params: URLSearchParams = new URLSearchParams(
window.location.search
);
const newStampId: string | null = params.get("stampId");
if (newStampId) {
// スタンプIDが指定されている場合、stampIdsに追加し、クッキーを更新する
addNewStamp(newStampId);
}
}, []);
Cookieには、user_id
として"6477f618-be29-4e59-a7d8-d29afdda271e"
のようなUUID文字列が、stamp_ids
として"stamp1,stamp2"
のようなカンマ区切りのスタンプID文字列が入ります。
描画
あとはStampID配列を描画するだけです。
スタンプカードに合わせて、スタンプIDごとに位置を定義しておいて、それをスタンプカードの画像の上に重ねて置くだけ。
<div style="position: relative;">
<img src="stamp_card.png" />
<img src="stamp1.png"
style="
position: absolute;
left: 291;
top: 173;"
/>
</div>
これをReactで書くだけなので、TypeScriptのコードは割愛。
基本的にはここまでです。
Next Step
機能を追加していきます。
1: 初期化
野外のイベントをやってたら何があるかわからないので、まっさらにしてやり直ししたいことがあるでしょうということで、URLパラメータとしてinitialize
が渡ってきたら、全部消すという処理を追加しました。
const [userId, setUserId] = useState<string>(getOrCreateUniqueUserId());
const [stampIds, setStampIds] = useState<string[]>(
getCookieValues("stamp_ids")
);
useEffect(() => {
// ・・・省略・・・
// クッキーの削除パラメータがあった場合は、削除する
const initializeParam: string | null = params.get("initialize");
if (initializeParam) {
deleteCookie('user_id');
deleteCookie('stamp_ids');
setUserId(getOrCreateUniqueUserId());
setStampIds([]);
}
}
// クッキーを削除する
export const deleteCookie = (key:string) => {
document.cookie = `${key}=; path=/; max-age=0`;
}
2: Cookieの利用拒否
ブラウザの設定で、Cookieの利用を拒否できるので、それを検知したいと思いました。変なセキュリティ意識の高い人が拒否ってそう。イベント中にQRコードを読み込んで「何も起きないんですけどー!」とか発生しうる。
意識高い系の人のためになんて対応はめんどくさいですが、現場でトラブルが起きるよりは、事前にやれることとしてやっておきます。
クッキーを書いて、すぐ読むことができるかどうか、というチェックです。
// Cookieが有効かどうかを確認する
export const checkCookieEnabled = (): boolean => {
const testKey = 'cookie_test';
document.cookie = `${testKey}=1; max-age=10; path=/`;
const match = document.cookie.match(new RegExp(`${testKey}=([^;]+)`));
const result = !!match;
// テスト用Cookieを削除(副作用防止)
document.cookie = `${testKey}=; max-age=0; path=/`;
return result;
};
それをstateに設定して、
const [cookieEnabled] = useState<boolean>(checkCookieEnabled());
描画する。
<div>
<p>ブラウザ設定で、Cookieが有効かどうか</p>
<p>{cookieEnabled ? '有効' : '無効'}</p>
</div>
「クッキーが無効なので使えませーん!」というメッセージを出す感じです。
3: 利用ログ収集
ちょっと別の視点で、スタンプが押されたら、Google APIを使ってBigQueryへデータを蓄積したいと思いました。このデータはクライアントでは使用せず、後日、分析に使う。行動の時系列データなので分析しがいがありそうです。売り上げと重ねたら面白い。
方法は、静的HTMLでも、どこかへPOSTするだけならできます。GASへPOSTして、GASがBQに入れればいいです。レコード件数が少なければ、Spread Sheetでもいい。とにかくここでは、POSTすることの技術的な確認をします。
useEffect()
から、次の関数を呼びます。
// スタンプ情報をPOSTするURL
const POST_URL = "http://hoge.com/api/stamp";
// スタンプが追加された旨をPOSTする(非同期で実行し、クライアント側へは特に通知しない)
export const postStamp = (userId:string, stampId:string) => {
fetch(POST_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, stampId }),
})
.then((res) => {
if (!res.ok) throw new Error('通信エラー');
return res.json();
})
.then(() => {
console.log(`スタンプ「${stampId}」を獲得しました!`);
})
.catch(() => {
console.log('通信に失敗しました。もう一度お試しください。');
});
}
console.log()
は、出しても出さなくてもいいけど。
ところで、Cookieの使用許諾?
最近よく「Cookieを使っていいか?」っていうメッセージ(同意バナーと呼ぶ)が出ますので、あれの説明。
まず、ブラウザの設定でCookieの使用可否。これが唯一の技術的な壁です。ここで拒否されていたら使えないし、許可されていたら使える。
では「Cookieを使っていいか?」とは何か? その質問に 「ダメ」と答えられたら、自主的に見ない というだけです。そこでどう答えようと、技術的には見られる。
ではなぜそれを聞くかというと、個人情報の扱いの問題。Cookieは個人情報であると解釈されることもあるので(特にヨーロッパ圏、GDPR対応というキーワードで調べて)、法的リスクの観点で危ない橋を渡らない ように、事前に確認して、OKなら使うしダメなら使わないということです。
日本では、IPアドレスは個人情報 です。これは確定。でも それ以外は微妙 で、特に単独では、どちらかというと個人情報ではないものが多い。(←判断は自己責任でお願いします。)なので、今回のスタンプカードのクッキーを保存して読むのはセーフと、私は考えました。
ということで、私が使おうとしているのは小さなイベントでもあるし、同意バナーは出さず、勝手にUUIDとStampIDを保存して読むことにします。
まとめ
技術的に簡単という話だったので、一瞬で終わりそうでしたが、改造の話を書いていたら長くなって疲れました。
スタンプカードの利用は、イベントではよくありそうだし、簡単に使えるのであれば使いたいですよね。私は、イベントのウェブサイトの1ページを使おうと思っています。
今ふと思いついたんですが、ウェブサイトの一部でもなく、本当にスタンプカードだけのシステムが欲しいなら、GASでhttpサーバー立てればできますね。コピペも簡単。忘れないうちに作っておこうかな🤔
ではよきスタンプライフを!