4
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?

More than 1 year has passed since last update.

ミロゴスAdvent Calendar 2023

Day 14

LIFFアプリの簡単な始め方について

Last updated at Posted at 2023-12-13

はじめに

この記事は、ミロゴス Advent Calendar 2023 14日目の投稿です。
本記事では、LIFF開発をするにあたって必要な内容を紹介できればと思います。

LIFFとは

LINE Front-end Framework(LIFF)は、LINEヤフー株式会社が提供するウェブアプリのプラットフォームです。
このプラットフォームで動作するウェブアプリを、LIFFアプリと呼びます。

LIFFアプリを使うと、LINEのユーザーIDなどをLINEプラットフォームから取得できます。
LIFFアプリではこれらを利用して、ユーザー情報を活用した機能を提供したり、ユーザーの代わりにメッセージを送信したりできます。

内容

環境構築

この記事では、liffの他に下記二つのライブラリを使用します。

  • nextjs
  • chakraui

環境構築をこちらの記事で紹介しているので、ご参照下さい。

パッケージの追加

  • LIFF SDKのインストール
npm install --save @line/liff

サーバーの準備

LIFFを始めるにあたって、サーバーを用意する必要があります。
そのため、今回はngorkというローカルサーバーを全世界に公開できる便利なツールを使用します。

ngrokの登録

  • ngrokのサインアップ

  • ngrokのインストール
brew install ngrok/ngrok/ngrok
  • 認証トークンをデフォルトのngrok.yml 構成ファイルへ追加
ngrok config add-authtoken xxxxxxxxxx
  • 設定ファイルの内容を表示
cat ~/Library/Application\ Support/ngrok/ngrok.yml

Webサーバーの公開

ローカルのポート3000で実行されているウェブサーバーを外部に公開します。

ngrok http 3000
ngrok                                                                                                                                                    (Ctrl+C to quit)
                                                                                                                                                                         
Build better APIs with ngrok. Early access: ngrok.com/early-access                                                                                                       
                                                                                                                                                                         
Session Status                online                                                                                                                                     
Account                       xxx@xxx.com (Plan: Free)                                                                                                 
Update                        update available (version 3.5.0, Ctrl-U to update)                                                                                         
Version                       3.3.4                                                                                                                                      
Region                        Japan (jp)                                                                                                                                 
Latency                       7ms                                                                                                                                        
Web Interface                 http://127.0.0.1:4040                                                                                                                      
Forwarding                    https://xxxxxxxxxx.ngrok-free.app -> http://localhost:3000                                                 
                                                                                                                                                                         
Connections                   ttl     opn     rt1     rt5     p50     p90                                                                                                
                              329     1       0.00    0.00    0.04    5.78                                                                                               
                                                                                                                                                                         
HTTP Requests                                                                                                                                                            
-------------       

LIFFの作成

LIFFを始めるための必要な準備をします。

チャネルを作成

LINE Developersコンソールへログイン

必要であれば、新規アカウント作成をします。

新規チャネルの作成
  • 「プロバイダー」 > 「チャネル」から「新規チャネルの作成」を押下します。
  • LINEログインを選択します。
    image.png
  • チャネル内のLIFFに関する設定を入力します。
    • 必須項目を選択・入力し、LINE開発規約に同意チェックを入れます。
      • アプリタイプについては、ウェブアプリを選択します。
LIFFの追加

作成したチャネルにLIFFアプリを追加します。

  • 作成したチャネルからLIFFタブを選択し、追加ボタンを押下します。
  • 必須の項目を入力します。
    • エンドポイントURLには、ngrokで作成したURLを入力します。
    • 項目の詳しい解説につきましては、以下の公式ガイドで確認ください。

LIFFの使い方

全体構成

今回、使用する全体の構成になります。
ソースコードにつきましては、以下の記事で作成したものを少し改変したものとなります。

ソースコード一覧
ディレクトリ構成
.
├── README.md
├── components
│   └── Index
│       ├── MainContent.tsx
│       └── constants.ts
├── constants
│   └── index.ts
├── hooks
│   └── useLiffInitialization.ts
├── next-env.d.ts
├── node_modules
├── package-lock.json
├── package.json
├── pages
│   ├── _app.tsx
│   └── index.tsx
├── public
│   └── images
│       ├── image_1.png
│       ├── image_2.png
│       └── image_3.png
├── tsconfig.json
├── types
│   └── index.ts
├── utils
│   └── sample-data.ts
└── .env
ソースコード
components/Index/constants.ts
export const TITLE_FIRST = 'タイトル1';
export const TITLE_SECOND = 'タイトル2';
export const TITLE_THIRD = 'タイトル3';
export const IMAGE_PATH_FIRST = '/images/image_1.png';
export const IMAGE_PATH_SECOND = '/images/image_2.png';
export const IMAGE_PATH_THIRD = '/images/image_3.png';
export const IMAGE_ID_VALIDATION_MESSAGE = '画像を選択してください。';
export const MESSAGE_FIELD_VALIDATION_MESSAGE =
  'メッセージを入力してください。';
export const MESSAGE_FIELD_MAX_LENGTH = 100;
export const MESSAGE_FIELD_MAX_LENGTH_VALIDATION_MESSAGE =
  'メッセージは100文字以内で入力してください。';
export const SUBMIT_BUTTON_TEXT = '送信';
components/Index/MainContent.tsx
import { Liff } from '@/types';
import { CheckIcon } from "@chakra-ui/icons";
import {
    Box,
    Button,
    Flex,
    FormControl,
    FormErrorMessage,
    Heading,
    Image,
    Text,
    Textarea,
    VStack,
} from "@chakra-ui/react";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import * as FormConstants from "./constants";

export interface MainContentProps {
    liffObject: Liff;
}

const MainContent = (props: MainContentProps) => {
    const { liffObject } = props;
    const images = [
        {
            id: 1,
            title: FormConstants.TITLE_FIRST,
            src: FormConstants.IMAGE_PATH_FIRST,
        },
        {
            id: 2,
            title: FormConstants.TITLE_SECOND,
            src: FormConstants.IMAGE_PATH_SECOND,
        },
        {
            id: 3,
            title: FormConstants.TITLE_THIRD,
            src: FormConstants.IMAGE_PATH_THIRD,
        },
    ];
    const [selectedImageId, setSelectedImageId] = useState(null as number | null);
    console.log("選択した画像ID", selectedImageId);

    const {
        register,
        handleSubmit,
        control,
        setValue,
        formState: { errors },
    } = useForm();

    useEffect(() => {
        setValue("imageId", selectedImageId);
    }, [selectedImageId, setValue]);

    const onSubmit = (data) => {
        const imageId = data.imageId;
        const message = data.message;

        console.log("取得した画像ID", imageId);
        console.log("取得したメッセージ", message);
    };

    return (
        <>
            <Flex alignItems="center" justifyContent="center">
                <VStack spacing="4" align="start" padding="0">
                    <VStack align="start">
                        <Heading size="lg">画像の選択</Heading>
                        <Heading fontSize="14px">
                            画像をクリックすると対象の画像IDを取得できます
                        </Heading>
                    </VStack>
                    <FormControl isInvalid={!!errors.imageId}>
                        <Controller
                            name="imageId"
                            control={control}
                            defaultValue=""
                            rules={{
                                required: FormConstants.IMAGE_ID_VALIDATION_MESSAGE,
                            }}
                            render={({ field }) => (
                                <input
                                    type="hidden"
                                    value={field.value || ""}
                                    onChange={field.onChange}
                                />
                            )}
                        />
                        {typeof errors.imageId?.message === "string" && (
                            <FormErrorMessage>{errors.imageId.message}</FormErrorMessage>
                        )}
                    </FormControl>
                    {images.map((image, index) => (
                        <React.Fragment key={index}>
                            <Text>{image.title}</Text>
                            <Box
                                position="relative"
                                key={image.id}
                                borderRadius="md"
                                onClick={() => {
                                    setSelectedImageId(image.id);
                                }}
                                marginBottom={index === images.length - 1 ? "30px" : "0"}
                            >
                                <Image
                                    src={image.src}
                                    alt={`Image ${image.id}`}
                                    width="100%"
                                    opacity={selectedImageId === image.id ? 1 : 0.6}
                                />{" "}
                                {selectedImageId === image.id && (
                                    <CheckIcon
                                        position="absolute"
                                        top="5px"
                                        right="5px"
                                        w={6}
                                        h={6}
                                        color="green.400"
                                    />
                                )}{" "}
                            </Box>
                        </React.Fragment>
                    ))}
                </VStack>
            </Flex>
            <Flex alignItems="center" justifyContent="center">
                <form onSubmit={handleSubmit(onSubmit)}>
                    <FormControl isInvalid={!!errors.message} width="500px">
                        {typeof errors.message?.message === "string" && (
                            <FormErrorMessage>{errors.message.message}</FormErrorMessage>
                        )}
                        <VStack align="start" marginBottom="20px">
                            <Heading size="lg">メッセージの入力</Heading>
                            <Heading fontSize="14px">入力された内容を取得します</Heading>
                        </VStack>
                        <Textarea
                            {...register("message", {
                                required: FormConstants.MESSAGE_FIELD_VALIDATION_MESSAGE,
                                maxLength: {
                                    value: FormConstants.MESSAGE_FIELD_MAX_LENGTH,
                                    message:
                                        FormConstants.MESSAGE_FIELD_MAX_LENGTH_VALIDATION_MESSAGE,
                                },
                            })}
                            height="200px"
                            border="2px solid black"
                            marginBottom="20px"
                        />
                        <Button
                            type="submit"
                            width="100%"
                            backgroundColor="black"
                            color="white"
                        >
                            {FormConstants.SUBMIT_BUTTON_TEXT}
                        </Button>
                    </FormControl>
                </form>
            </Flex>
        </>
    );
};

export default MainContent;
hooks/useLiffInitialization.ts
import { Liff } from '@/types/index';
import { useEffect, useState } from 'react';

// LIFFの初期化に必要なプロパティを定義した型
export interface LiffInitializationProps {
  // LINE Developers Consoleで取得したアプリのLIFF ID
  liffId: string;
}

// LIFFの初期化結果を表す型
export interface LiffInitialization {
  liffObject: Liff | null;
  isLiffError: boolean;
}

const useLiffInitialization = (
  props: LiffInitializationProps
): LiffInitialization => {
  const { liffId } = props;
  const [liffObject, setLiffObject] = useState<Liff | null>(null);
  const [isLiffError, setIsLiffError] = useState<boolean>(false);

  // useEffectを使って初回レンダリング時やliffIdが変更されたときに実行される処理
  useEffect(() => {
    const initializeLiff = async () => {
      try {
        const liffModule = await import('@line/liff').then(
          (module) => module.default
        );
        console.log('LIFF init...');
        await liffModule.init({ liffId: liffId }); // LIFFの初期化を行う
        console.log('LIFF init success.');

        // ログインしていない場合、ログイン処理を実行
        if (!liffModule.isLoggedIn()) {
          console.log('User not yet logged in.');
          liffModule.login();
        }

        await liffModule.ready; // LIFFの準備完了を待つ
        console.log('LIFF ready.');
        setLiffObject(liffModule); // LIFFオブジェクトをReactステートにセット
      } catch (error) {
        console.log('LIFF init failed.');
        setIsLiffError(true);
      }
    };

    initializeLiff(); // 上記の初期化処理を実行
  }, [liffId]); // liffIdが変更されたときに再実行するように指定

  // レンダリング結果としてLIFFオブジェクトとエラーフラグを返す
  return { liffObject, isLiffError };
};

export default useLiffInitialization;
constants/index.ts
export const LIFF_ID = process.env.NEXT_PUBLIC_LIFF_ID || '';
pages/index.tsx
import MainContent from '@/components/Index/MainContent';
import { LIFF_ID } from '@/constants/index';
import useLiffInitialization from '@/hooks/useLiffInitialization';
import { Box, Flex, Text } from '@chakra-ui/react';


const IndexPage = () => {
  const liffId: string = LIFF_ID;
  const { liffObject, isLiffError } = useLiffInitialization({ liffId }); // LIFFの初期化を行うカスタムフックから結果を受け取る

  // もし初期化中にエラーが発生した場合、エラーメッセージを表示
  if (isLiffError) {
    return (
      <Flex align="center" justify="center" height="100vh">
        <Box>
          <Text>エラーが発生しました</Text>
        </Box>
      </Flex>
    );
  }

  // もしLIFFオブジェクトがまだ取得できていない場合、何も表示せずに終了
  if (!liffObject) {
    return null;
  }

  return <MainContent liffObject={liffObject} />;
};

export default IndexPage;
types/index.ts
import liff from '@line/liff';

export type Liff = typeof liff;
.env
NEXT_PUBLIC_LIFF_ID=(作成したLIFF ID)

LIFF IDのセット

作成したチャネルのLIFF IDを呼び出せるように設定します。

  • env ファイルへLIFF_IDを追加
.env
NEXT_PUBLIC_LIFF_ID=(作成したLIFF ID)

.env ファイル内で NEXT_PUBLIC_LIFF_ID という環境変数に LIFF ID を設定しています。
この設定は、環境変数としてアプリケーション内で使えるようになります。

  • NEXT_PUBLIC_LIFF_ID を LIFF_ID として出力
constants/index.ts
export const LIFF_ID = process.env.NEXT_PUBLIC_LIFF_ID || '';

環境変数 NEXT_PUBLIC_LIFF_ID を LIFF_ID としてエクスポートしています。
デフォルト値として空の文字列を使用しています。

LIFFの初期化についてのReactフック

このReactフックは、LIFFの初期化を行うための処理を提供します。
以下は、このフックが行っている主な処理です。

useLiffInitialization.ts
  • 初期化プロパティの型定義
export interface LiffInitializationProps {
  liffId: string; // LINE Developers Consoleで取得したアプリのLIFF ID
}

フックのプロパティとして、LINE Developers Consoleで取得したアプリのLIFF ID が必要です。

  • 初期化結果の型定義
export interface LiffInitialization {
  liffObject: Liff | null; // 初期化されたLIFFオブジェクト
  isLiffError: boolean; // 初期化中にエラーが発生したかどうかを示すフラグ
}

フックはLIFFの初期化結果を提供し、初期化されたLIFFオブジェクトとエラーフラグが含まれます。

  • Reactフックの実装
const useLiffInitialization = (
  props: LiffInitializationProps
): LiffInitialization => {
  // ...(省略)

  useEffect(() => {
    const initializeLiff = async () => {
      try {
        // LIFFモジュールの動的インポート
        const liffModule = await import('@line/liff').then(
          (module) => module.default
        );

        // LIFFの初期化
        await liffModule.init({ liffId: liffId });

        // ログインしていない場合、ログイン処理を実行
        if (!liffModule.isLoggedIn()) {
          liffModule.login();
        }

        // LIFFの準備完了を待つ
        await liffModule.ready;

        // LIFFオブジェクトをReactステートにセット
        setLiffObject(liffModule);
      } catch (error) {
        // 初期化中にエラーが発生した場合
        setIsLiffError(true);
      }
    };

    initializeLiff(); // 初期化処理を実行
  }, [liffId]); // liffIdが変更されたときに再実行するように指定

  // レンダリング結果としてLIFFオブジェクトとエラーフラグを返す
  return { liffObject, isLiffError };
};

フックはuseEffectを使用して、初回レンダリング時やLIFF IDが変更されたときに LIFF を初期化します。
初期化が成功したら準備完了まで待ち、LIFFオブジェクトをReactステートにセットします。
エラーが発生した場合は、エラーフラグがセットされます。

このフックを使用することで、LIFFを簡単かつ効果的にReactアプリケーションに統合できます。

LIFFオブジェクトのセットとメインページの表示について

LIFF(LINE Frontend Framework)の初期化が成功した後に、liffObjectを使用してメインコンテンツを表示するReactページを示しています。

  • LIFF IDの取得
const liffId: string = LIFF_ID;

LINE Developers Consoleで取得したアプリのLIFF ID が LIFF_ID として定義され、それを liffId に代入しています。

  • LIFFの初期化
const { liffObject, isLiffError } = useLiffInitialization({ liffId });

カスタムフック useLiffInitialization を使用して、LIFFの初期化を行います。
liffObject は初期化されたLIFFオブジェクトで、isLiffError は初期化中にエラーが発生したかどうかを示すフラグです。

  • エラーの表示
if (isLiffError) {
  // エラーメッセージを表示
  return (
    <Flex align="center" justify="center" height="100vh">
      <Box>
        <Text>エラーが発生しました</Text>
      </Box>
    </Flex>
  );
}

もし初期化中にエラーが発生した場合、エラーメッセージを表示します。

  • LIFFオブジェクトが取得できない場合の処理
if (!liffObject) {
  // LIFFオブジェクトがまだ取得できていない場合は何も表示せずに終了
  return null;
}

もし LIFFオブジェクトがまだ取得できていない場合は、何も表示せずに終了します。

  • メインコンテンツの表示
return <MainContent liffObject={liffObject} />;

LIFFオブジェクトが取得できた場合は、MainContent コンポーネントに liffObject を渡してメインのコンテンツを表示します。

画面表示

以下の画面が正常に表示されればOKです。

  • 作成したチャネルのLIFF URLへ遷移します。
    image.png

  • ngrokの確認画面が表示されるため、「visit Site」を押下します。
    image.png

  • LINEログイン画面が表示されているため、ログインします。
    image.png

  • 作成したチャネルのエンドポイントURLで設定したURLへ遷移されます。
    image.png

さいごに

LIFFは、LINEプラットフォームを活用した柔軟なアプリケーションの開発を可能にするツールです。
このツールを通じて、開発者はLINEユーザーとの対話を強化し、新しい機能やサービスを提供することができます。
機能面については数多くあるため、別の機会で紹介できればと思います。

4
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
4
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?