0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LINE WORKSAdvent Calendar 2024

Day 21

Next.js を使って WOFF に入門する話

Last updated at Posted at 2024-12-20

はじめに

こんにちは。2024 年ももう終わりですね。はやすぎ〜。

最近は LINE WORKS アプリで動く Web アプリ、いわゆる WOFF アプリを作る機会があったので、
LINE WORKS アドベントカレンダー 2024 のネタとして、
本稿では React / Next.js を用いた WOFF アプリの構築方法を皆様に共有したいと思います。

WOFF とは

  • WOFF(WORKS Front-end Framework)は、LINE WORKS が提供する Web アプリプラットフォームです。

  • LINE WORKS アプリには、WOFF アプリを実行するための専用ブラウザ、WOFF ブラウザがあります。

  • モバイル版 LINE WORKS アプリのトークルームからWOFF URLにアクセスすると、WOFF ブラウザが動作します。

    • WOFF URLは、後述の Developer Console でアプリを登録すると発行される専用の URL です。
      • この URL を経由した場合のみ WOFF ブラウザが動作し、それ以外の場合は通常の In-app ブラウザが開かれます。

環境設定

Developer Console へのアプリ登録

  • Developer Console は LINE WORKS アプリの設定を行うためのページです。

    • 使用するためにはアクセス権限が必要です。
    • もし権限を持っていない場合、以下の記事を参考に権限をお持ちのメンバーに追加を依頼してみてください。
  • 右側の API ClientApp メニューから「アプリの新規追加」を選択します。

    • 作成したアプリ内の「WOFF アプリ」の「登録」を選択し、WOFF アプリをデプロイするためのエンドポイントを登録します。
    • 登録後、WOFF URL が発行されます。
  • 発行された WOFF URL にアクセスすると、WOFF ブラウザでトークルームを開くことができます。

    • この段階で、モバイルサイズ向けに実装された Web アプリを WOFF アプリとして使用することが可能になります。
  • 例: create-next-app にて作成した初期状態の Next.js アプリを動作させているサーバーに WOFF URL からアクセス。

    • 特に WOFF SDK を使っていませんが、WOFF ブラウザを活用することが可能です。

create-next-app

WOFF SDK

  • LINE WORKS ユーザー情報の取得やトークルームへのメッセージ送信を行いたい場合は、WOFF SDK を利用して機能を実装できます。

    • WOFF ブラウザの利用のみであれば、Developer Console だけで環境設定が可能です。
  • 利用例

    • WOFF SDK を通して取得(woff.getAccessToken())したトークンを使用して、バックエンド側で LINE WORKS API を利用した機能を実装する。
    • WOFF で処理した結果をメッセージにしてトークルームに転送する。
  • Reference

Next.js への導入

  • WOFF SDK は 2024 年 12 月初旬現在、JavaScript の静的ファイルとして配布されているため、通常<script>タグを使ってロードします。
<script
  charset="utf-8"
  src="https://static.worksmobile.net/static/wm/woff/edge/3.6.2/sdk.js"
></script>
  • Next.js では<Script> Componentが提供されているため、こちらを利用することが可能です。
<Script strategy="afterInteractive" src="https://static.worksmobile.net/static/wm/woff/edge/3.6.2/sdk.js">

スクリプトがロードされると、window オブジェクト内にwindow.woffというオブジェクトが登録されます。
woffオブジェクトとしても参照できますが、本稿ではwindowから参照します。

型定義

2024 年 12 月中旬現在、公式の型定義は提供されていません。

そのため、TypeScript の恩恵を利用したい筆者は、ドキュメントを参照しながら手動で型定義を作成しました。

/** @see https://developers.worksmobile.com/jp/docs/bot-send-flex */
interface FlexTemplate {
  type: "flex";
  altText: string;
  contents: object;
  i18nAltTexts?: Array<{ language: string; altText: string }>;
}

/** Woff SDK interface
 * Unknown functions are defined as () => void
 * You need to check & modify if it's match how these works
 * @see https://developers.worksmobile.com/jp/docs/woff-api
 */
export interface Woff {
  checkFeature: () => void;
  closeWindow: () => void;
  getAId: () => void;
  getAccessToken: () => string;
  getContext: () => {
    viewType: "compact" | "tall" | "full";
    endpointUrl: string;
    permanentLinkPattern: string;
    clientId: string;
    clientType: "PC_WEB" | "MOBILE_APP" | "PC_APP";
  };
  getDecodedIDToken: () => void;
  getFeatures: () => void;
  getFriendship: Fucntion;
  getIDToken: () => void;
  getIsVideoAutoPlay: () => void;
  getLanguage: () => string;
  getOS: () => "ios" | "android" | "web";
  getProfile: () => Promise<{
    domainId: string;
    userId: string;
    displayName: string;
  }>;
  getProfilePlus: () => void;
  getVersion: () => string;
  getWorksVersion: () => string;
  init: (
    config: { woffId: string },
    successCallback: () => void,
    errorCallback: () => void
  ) => Promise<void>;
  isApiAvailable: () => boolean;
  isInClient: () => boolean;
  isLoggedIn: () => boolean;
  login: (loginConfig: { redirectUri?: string; domain?: string }) => void;
  logout: () => void;
  openWindow: (params: { url: string; external: boolean }) => void;
  permanentLink: {
    createUrl: () => void;
    setExtraQueryParam: () => void;
  };
  ready: Promise<void>;
  scanQR: () => Promise<string>;
  sendCalendarVoipUrl: () => void;
  sendFlexMessage: (message: { flex: FlexTemplate }) => Promise<void>;
  sendMessage: (message: { content: string }) => Promise<void>;
  sendMessages: () => void;
  shareTargetPicker: () => void;
  userPicker: () => void;
}

export interface WoffError {
  code: string;
  message: string;
}

declare global {
  interface Window {
    woff: Woff;
  }
}

Window オブジェクトを拡張し、window.woffを追加しています。
プロジェクトのtsconfig.jsonincludeに指定することで使用可能になります。

  // WOFF type definitions: `src/types/woff.d.ts`
  "include": ["next-env.d.ts", "src/types/woff.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],

SDK の初期化

上記では Next.js のScript コンポーネントを SDK スクリプト読み込みに使用しました。

これを用いることにより、例えば SDK スクリプトのロードイベントにて、WOFF SDK の初期化処理を以下のように追加できます。

<Script
  strategy="afterInteractive"
  src={WOFF_SDK_SRC}
  onLoad={() => {
    // WOFF SDK initialization on loading sdk.js
    window.woff.init({ woffId: YOUR_WOFF_ID }, successHandler, errorHandler);
  }}
/>

woff.init() による初期化完了時のイベントに対して、任意の処理を第 2 引数, 第 3 引数から渡すことができます。

/** This is called when initialization of woff succeeds */
const successHandler: Parameters<Woff["init"]>[1]> = async () => {
  await window.woff.ready;
  // Add your success handling
};

/** This is called when initialization of woff fails */
const errorHandler: Parameters<Woff["init"]>[2]> = () => {
  // Add your error handling
};

サンプルアプリの作成

WOFF SDK の練習として、トークルームにお絵描きを投稿するというシンプルなアプリを Next.js(app router)で作成してみました。

お絵描き部分にはreact-sketch-canvas (MIT License) の Example を参考にしながら利用しました。

Developer Console から発行されたWOFF URLにトークルームからアクセスするとこのような形で見えます。

絵心がないのでとりあえず文字を書きます。。。

canvas.jpg

Next.js project の作成

  • create-next-app コマンドを用いて、Next.js プロジェクトを新規に作ります。
    • 使用したい環境に Doc を参考に合わせてオプションをつけます。(e.g. Typescript, eslint, tailwind css)
    • Would you like to use App Router? (recommended) のような質問に Yes/No で回答するとプロジェクトが作成できます。
      • 今回は app router を用いることにしました。
npx create-next-app@latest my-project --typescript --eslint --tailwind

WOFF SDK の読み込み

  • このサンプルアプリでは app/layout.tsx にスクリプトのロードなどを配置しました。
    • 上述Script をラップしたWoffScriptLoader Component を作成しました。
    • 別途アプリ全体へのステート共有のための Context Provider も配置します。
      • SDK の初期化成功時にステートを更新し、子コンポーネントへ通知するために使っています。
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      {/* SDKの初期化成功済を共有するステートをアプリケーション全体に共有 */}
      <WoffStateProvider>
        <Suspense>
          {/* SDK の読み込みを行う部分 */}
          <WoffScriptLoader />
        </Suspense>
        <body
          className={`${geistSans.variable} ${geistMono.variable} antialiased`}
        >
          {children}
        </body>
      </WoffStateProvider>
    </html>
  );
}

お絵描きの保存、トークルームへの送信

  • お絵描きに使用するReactSketchCanvas では ref Object を渡すことができ、それを経由して画像を Export できます。
  • これをトークルームへ Flex Message 機能を用いて送信することを考えます。
import { FC, useRef, useState } from "react";
import { ReactSketchCanvas, ReactSketchCanvasRef } from "react-sketch-canvas";

export const Sketch: FC = () => {
  const canvasRef = useRef<ReactSketchCanvasRef>(null);
  const [strokeColor, setStrokeColor] = useState("#000000");
  const [mode, setMode] = useState<"draw" | "erase">("draw");

  return (
    <div className="flex size-full flex-col gap-4">
      <div className="flex items-center justify-center gap-4">
        <ReactSketchCanvas ref={canvasRef} strokeColor={strokeColor} />
        {/* switches here... */}
      </div>
    </div>
  );
};

送信ボタンハンドラの実装

  • Post ボタンを押した際にお絵描きした画像をトークルームに送信するように実装します。
  • 送信前の準備として以下のような処理をボタンハンドラで行いました。
    • キャンバス上のお絵描きを.png の Data URL に変換
    • バックエンドに渡してトークルームからもアクセス可能な URL を発行
const pngDataUrl = await canvasRef.current.exportImage("png");

// Execute API to store image & publish image URL
const response = await fetch("/api/store", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    file: { name: `${uuidv4()}.png`, data: pngDataUrl },
  }),
});
  • Flex Message 機能では画像 URL を使ってメッセージに画像をのせることができます。
    • URL には制約 があり、PNG 形式, 1000 文字以内, HTTPS のみ可能です
  • Flex Message 用 API woff.sendFlexMessageを用いて WOFF アプリを開いているトークルームへメッセージを送信します。
    • WOFF ブラウザが使用可能な LINE WORKS モバイルアプリ以外では使用できません。
    • WOFF ブラウザを使用しているかどうかはwoff.isInClient()で判定が可能です。
// Make sure if the browser is WOFF or another.
if (window.woff.isInClient()) {
  // Send flex message with image to WORKS Talkroom
  window.woff.sendFlexMessage({ flex: crateFlexMessage(json.url) });
}
  • 画像送信用の Flex Template は以下のようになっています
    • ガイドを参考にカスタマイズが可能ので、ぜひ試してみてください。
const createFlexMessage = (url: string): FlexTemplate => {
  return {
    type: "flex",
    altText: "Your sketch",
    contents: {
      type: "bubble",
      body: {
        type: "box",
        layout: "vertical",
        contents: [
          {
            type: "image",
            url,
            size: "full",
            aspectRatio: "2:1",
          },
        ],
      },
    },
  };
};

画像 URL の発行

  • 画像 URL 発行用には、バックエンドには練習を兼ねて Next.js の API および Supabase を使用しました

  • Next.js API ルートを実装し、サーバー上から supabase の操作を行なっていきます。

  • 二度手間になりますが、まずはクライアント側から渡された DataURL をバイナリデータにもどします。

export async function POST(request: NextRequest) {
  // .... skip ....

  // Remove the data URL prefix to get the base64-encoded data
  // because the data URL is not supported by the Buffer constructor
  const base64Data = data.replace(/^data:image\/\w+;base64,/, "");
  const buffer = Buffer.from(base64Data, "base64");
  • 変換したバイナリデータをsupabase-jsを使用して storage に保存します。
  • 保存ができたら画像 URL を発行してクライアントに返します。
    • 必要に応じて、クライアントから HTTPS でアクセスできるように画像 URL をプロキシします。
// Upload the image to the specified bucket
const { error } = await supabase.storage
  .from(BUCKET_NAME)
  .upload(name, buffer, {
    upsert: true,
    contentType: "image/png",
  });

// ... error handling ... //

// Publish URL
const { data: urlData, error: urlError } = await supabase.storage
  .from(BUCKET_NAME)
  .createSignedUrl(name, 60 * 60);

// ... error handling ... //

const proxiedUrl = replaceToProxiedUrl(urlData.signedUrl);

// Return to client
return new NextResponse(
  JSON.stringify({
    message: `Image uploaded successfully: ${name}`,
    url: proxiedUrl,
  }),
  {
    status: 200,
    headers: {
      "Content-Type": "application/json",
    },
  }
);

送信結果

トークルームに投稿されました。やったね!!

talk.jpg

ハマったポイント

ローカル実行時の初期化エラー

  • Developer Console に登録する Endpoint 以外から WOFF を実行すると、WOFF SDK 初期化時にエラーが生じます
  • Local 環境でデバッグしたい時のために、WOFF アプリを別途 Developer Console へ登録して用意しておくと捗ったので、個人的におすすめです
    • Developer Console 画面からlocalhost の URL を Endpoint に指定した WOFF アプリを登録します
  • 初期化時に WOFF ID が設定されていないと、エラーとなるので設定忘れにも注意しましょう
    • 環境変数などに発行された WOFF ID を保持しておき、初期化の際に使用できるようにしておきます
    • ログインをしておかないと、WOFF SDK の大半の機能は使えないため、初期化タイミングなどでログインをさせます
      • WOFF ブラウザのみでの実行する場合は不要です
      • 現在は HTTPS で登録しても、下記のように指定しておくと localhost への HTTP URL にリダイレクトされます。
if (!window.woff.isLoggedIn()) {
  window.woff.login({
    redirectUri: "http://localhost:3000",
  });
}

OAuth Scope の設定

  • Developer Console のアプリ登録画面からOAuth Scopeの設定が可能です
    • 筆者は設定前にwoff.loginを実行し、何度もエラー画面に遷移するという挙動にハマりました
    • 最低限user.profile.read を設定するとログイン可能になります
      • WOFF SDK ではwoff.getProfile のようにユーザー情報の取得が可能になるためこちらの設定が必要になります
    • メッセージを送る場合はbot またはbot.message の権限が必要です

まとめ

  • 本稿では WOFF SDK と Next.js を組み合わせた Web アプリの作成方法を紹介しました。
  • WOFF SDK は LINE WORKS 特有の機能を使いたい時に役に立ちます。
    • トークルームにメッセージ送信したい時
    • WORKS API を使用するバックエンドと連携したい時
    • WOFF ブラウザの開閉を利用したい時
  • サンプルアプリではお絵描きした画像をLINE WORKSのトークルームに送信することができました。

みなさんも WOFF で色々なアイデア試してみてください。良いお年を :thumbsup:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?