LIFF のバグレポートやフィードバックを円滑にする liff-screenshot-plugin を公開しました

Image from Gyazo

2022年4月にLIFFのプラグインシステムがリリースされたため、サードパーティプラグインとして『liff-screenshot-plugin』を開発・公開してみました。

GitHub | NPM

開発のモチベーション

スマートフォンブラウザに向けたアプリケーション(以下モバイル向けのアプリケーション)開発では、端末ごとに違う表示や動作が行われ、大小様々なバグが起こることがあります。特に端末依存のバグは OS の種類やバージョンによって動作が変わることもしばしばあり、実際にユーザーからの FB があってその詳細に気づく場合も少なくないのが現実です。

また、モバイル向けのアプリケーションでは小さなUIの中で多くの機能を表現するため、製作者や開発者の意図がエンドユーザーに正しく伝わりきるとも言えない状況もまた現実です。

実際私も業務で LIFF ではない環境を含めて LINE の In-App Browser 上で動作するアプリを開発することがありますが、どうしてもトラブルシューティングに時間がかかるケースは多くあります。

そんなとき、実際の画面のスクリーンショットがあれば、私達はもっと高速に問題の解決に取り組むことができます。そしてそのスクリーンショットも、 OS などで撮影されたプライバシーが関わるものではなく、あくまでもアプリケーションの現状であれば、取り扱いに困ることもなく気軽に利用できるはずです。

そんな問題を解消しつつ、スクリーンショットという扱いづらい部分の要件を満たした解決策として liff-screenshot-plugin を作成・公開しました。

内部構造としては DOM をそのまま Canvas を経由して画像として保存しているだけであり、外部との通信などを介さないシンプルなスクリーンショットのシステムとして、 liff.use するだけで使えるものとなっています。

導入方法について

他のパッケージと同様、 NPM でインストールできます。

$ npm i liff-screeenshot-plugin # or yarn add liff-screenshot-plugin

また、 @line/liff も NPM で提供されているため、合わせてインストールしたあとに、 liff.use をするだけで導入完了です。

import liff from '@line/liff'

import 'liff-screenshot-plugin/styles/index.css'
import LIFFSSPlugin from 'liff-screenshot-plugin'

liff.use(LIFFSSPlugin)

プラグインでできること

大きく2つのシチュエーションで利用できるので、それぞれ紹介します。

※ 本プラグインでは、取得した画像のアップロード機能は意図的にサポートしていません。
一方で実用する際には適切なサービス連携が必要となるため、後半で導入方法を紹介しています。

現在表示している画面のスクリーンショットの取得

Image from Gyazo

最もシンプルな機能として、 liff.$SS.capture 関数によるスクリーンショットの撮影を提供しています。こちらは surface の UI の状況やプラットフォームに関わらず、関数が call された時点での DOM 構造をそのまま Blob 形式の png 画像として出力します。サンプル画像は Mac で撮影していますが、もちろんモバイルの LIFF App 上で動作します。

Promise<Blob> を返すので扱いやすく、特別なオプションは現状存在しません。実際のコードでは、以下のように記述して利用します。

async function getScreenShot() {
  const blob = await liff.$SS.capture('blob') // 第一引数には現状 'blob' を指定します。

  // キャプチャしたデータは Blob で提供されるため、その後は自由に利用できます。
  const url = URL.createObjectURL(blob)

  // ...
}

スクリーンショット送信モーダルの表示

Image from Gyazo

もう一つ、LIFF向けに最適化した機能として、スクリーンショットに注釈をつけた上で送信できるモーダル型 UI の提供も行っています。こちらもサンプル画像は Mac で撮影していますが、もちろんモバイルの LIFF App 上で動作します。

こういった UI を含むコンポーネントは積極的に利用されることが多くはないですが、LIFF 上では UI コンポーネントの需要が生まれる可能性を検討し実装しています。

LIFF アプリは等しく LINE App 上で提供されるため、プラグインが UI を持ち、統一的なユーザー体験を提供できることは価値となることでしょう。

例えば LINE Pay で決済をするプラグインが誕生したとき、その UI が統一されていれば、ユーザーはどの LIFF アプリを利用しているときでも、統一的なユーザー体験のもとに決済が可能となります。

本プラグインでも、そのような統一的な体験を提供する意味でもモーダル型のUIを提供しています。

async function getScreenShot() {
  try {
    const result = await liff.$SS.captureWithModal('blob') // 第一引数には現状 'blob' を指定します。
    const blob = result.data

    // 入力されたテキストは feedback に格納されます
    console.log(result.feedback)

    // キャプチャしたデータは Blob で提供されるため、その後は自由に利用できます。
    const url = URL.createObjectURL(blob)

    // ...
  } catch(e) {
    // エラー発生時や中断された場合の処理を記述します
  }
}

また、モーダルの文言はデフォルト値であるため、必要であればサービスごとに合わせたものに書き換えてください。以下の型定義のもと、デフォルト値が定義されています。

export type TextDictionary = {
  title: string
  placeholder: string
  note: string
  cancelText: string
  submitText: string
}

const defaultTextDictionary: TextDictionary = {
  title: 'フィードバックを送信',
  placeholder: 'フィードバックについての詳細をご記入ください',
  note: 'いただいた情報ならびにスクリーンショットは、当サービス利用規約・プライバシーポリシーに則った範囲で利用いたします。',
  cancelText: 'キャンセル',
  submitText: '送信'
} as const

書き換える際は、第二引数に dictionary を渡します。詳細は README を参照ください。

取得したスクリーンショットのアップロードについて

本プラグインでは、スクリーンショットのアップロード機能自体は提供していません。

ユーザーのブラウザ上での画面を記録する特性上、サービスごとに決められた規約・プライバシーポリシーに則った管理が必要となるためです。

とはいえできるだけ低コストで試したい・導入したいというモチベーションもあると考えられるため、ここでは 2 つ参考となる実装を載せておきます。

Firebase Storage へのアップロード

最も手軽な方法はオブジェクトストレージへのアップロードです。例えば Firebase の Firebase Storage では、以下のようなコードを記述するだけで、実際の処理はたった数行でスクリーンショットをアップロードできます。

import { initializeApp } from 'firebase/app'
import { getStorage, ref, uploadBytes } from "firebase/storage"

initializeApp({
  storageBucket: "<project-name>.appspot.com"
})

const app = getApp();
const storage = getStorage();

async function upload(blob: Blob, uniqueId: string) {
  const name = `screenshot-${uniqueId}.png`
  const ssRef = ref(storage, `ss/${name}`);
  await uploadBytes(ssRef, result)
}

// ...logic

async function main() {
  const blob = await liff.$SS.capture('blob')
  await upload(blob, `${crypto.getRandomValues(new Uint32Array(10))}-${new Date().getTime()}`)
}

モーダル型 UI を利用する場合、 text/plain 形式で保存することにより、まとめて保存もできます。

Write Only で Read のみ管理者ができる状態であれば、 Firebase Storage に限らず、 AWS S3 などその他のオブジェクトストレージの利用も検討できます。

考えられるユースケース

機能や導入ガイドは上記で全てですが、あわせていくつか考えられるユースケースを記しておきます。

お問い合わせの一環として

単純に capture 関数で裏でスクリーンショットを取る以上ことをする場合、モーダル UI をそのまま利用して、お問い合わせに利用してもらうのが一番無難な利用方法になると考えられます。

本プラグインのモーダル UI は Google が提供している、サービス内でのフィードバックモーダルをベースに作られています。

Image from Gyazo

Google のフィードバックモーダル

そのため、LIFFアプリにおいても、同様のシチュエーションがもっともプラグインのパワーをローコストに発揮できる領域となります。基本的には、サービス内でフィードバックを受けたい箇所と liff.$SS.showModal を紐付けて、必要に応じてユーザーからの報告を受けるような実装が効果的でしょう。

エラーハンドリング処理の最終行程として

その他の利用方法として、ユーザーの同意を得た上で、エラーハンドリングの最後の Catch においてスクリーンショットを送信するようなユースケースも考えられます。

特定のデバイスにおいて不具合が起きているときに限定して、実際の表示がどうなっているかを確認したい。ということはそれなりの頻度で発生します。

最近は KARTE などユーザーの行動を一定期間 Web サービスの上のスクリーンショットや動画として記録するサービスも増えており、その中ではエラー表示もまるっと取り扱えるものもありますが、全てのサービスに重厚な SaaS が必要というわけではありません。

Image from Gyazo

Karte Live

そのようなシチュエーションにおいて、 LIFF のトラブルシューティングに限定してライトに利用する場合にも活用できます。

特に Sentry あたりで catch している処理が実装済みであれば、規約同意済みユーザーに限定してついでにスクリーンショットを送信するなども有効な手段と言えそうです。

おわりに

LIFF のプラグインシステムはまだ事例が多くないみたいなので、試す意味も込めて、今回は実際に業務で LINE の In-App Browser でサービスを提供しているときのペインポイントを解消できるプラグインを開発してみました。

アプリケーションの本質的な部分に関わってくるものは LIFF プラグインとして提供するのはリスキーに感じますが、一方で Developer Experience に関わる部分や Super App のプラットフォームとして統一的な体験が提供されることでユーザーに価値をもたらす部分はプラグイン化の恩恵が大きいと感じています。

オフィシャルに提供されている Mock や Inspector は、 LIFF アプリが通常の SPA 開発と比べて劣っている部分を補填するような存在ですが、課題を解消するだけでなく、より高い生産性をもってアプリケーションを開発できる基盤に成長してほしいですね。