【コード公開】LIFEアプリ作成手順|LINEログイン機能を実装する方法②

【コード公開】LIFEアプリ作成手順 DX化
【コード公開】LIFEアプリ作成手順

ReactでLINEログイン機能を実装する方法【コード付き解説】

【コード公開】LIFEアプリ作成手順|LINEログイン機能を実装する方法①の続きです。

ここからは、実際のコードを交えながら、Reactアプリケーションへのログイン機能の実装方法を詳しく解説します。フォルダ構成は前の記事を参考にしてください。

LIFFの初期化とログイン処理の記述

次に、LINEログインボタンとログイン処理を実装します。以下はシンプルなログインコンポーネントの例です。 

LoginComponent.jsx

import { useEffect, useState } from 'react';
import liff from '@line/liff';
import { useNavigate } from 'react-router-dom';

function LoginComponent() {
  const navigate = useNavigate();
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [userProfile, setUserProfile] = useState(null);
  const [error, setError] = useState("");

  // LIFF初期化
  useEffect(() => {
    // 環境変数からLIFF IDを取得(.envファイルに設定)
    const liffId = process.env.REACT_APP_LIFF_ID || import.meta.env.VITE_LIFF_ID;
    
    const initializeLiff = async () => {
      try {
        await liff.init({ liffId });
        console.log("LIFF初期化が完了しました");
        
        // すでにログインしているかチェック
        if (liff.isLoggedIn()) {
          setIsLoggedIn(true);
          fetchUserProfile();
        }
      } catch (e) {
        setError(`LIFF初期化エラー: ${e.message}`);
        console.error("LIFF初期化エラー", e.message);
      }
    };
    
    initializeLiff();
  }, []);
  
  // ユーザープロフィール取得
  const fetchUserProfile = async () => {
    try {
      const profile = await liff.getProfile();
      setUserProfile(profile);
      console.log("ユーザープロフィール取得成功:", profile);
      
      // ログイン後にマイページなどへリダイレクト
      // 少し遅延を入れるとスムーズに遷移します
      setTimeout(() => {
        navigate('/mypage', { state: { profile } });
      }, 500);
    } catch (e) {
      setError(`プロフィール取得エラー: ${e.message}`);
      console.error("プロフィール取得エラー", e.message);
    }
  };
  
  // LINEログイン処理
  const handleLogin = () => {
    if (!liff.isLoggedIn()) {
      // ログインしていなければ、ログイン処理を開始
      liff.login();
    }
  };
  
  // ログアウト処理
  const handleLogout = () => {
    liff.logout();
    setIsLoggedIn(false);
    setUserProfile(null);
    console.log("ログアウトしました");
  };
  
  // エラーがあれば表示
  if (error) {
    return <div className="error-message">{error}</div>;
  }
  
  // ログイン済みの場合、ユーザー情報を表示
  if (isLoggedIn && userProfile) {
    return (
      <div className="profile-container">
        <h2>ログイン済み</h2>
        <img 
          src={userProfile.pictureUrl} 
          alt="プロフィール画像" 
          className="profile-image" 
        />
        <p>名前: {userProfile.displayName}</p>
        <button onClick={handleLogout} className="logout-button">
          ログアウト
        </button>
      </div>
    );
  }
  
  // 未ログインの場合、ログインボタンを表示
  return (
    <div className="login-container">
      <h2>LINEでログイン</h2>
      <button onClick={handleLogin} className="line-login-button">
        LINEログイン
      </button>
    </div>
  );
}

export default LoginComponent;

上記のコンポーネントは、以下の機能を持っています。

  1. useEffect内でLIFFの初期化を行い、すでにログインしているかをチェックします
  2. ログインボタンのクリックでliff.login()を実行し、LINE認証画面を表示します
  3. ログイン成功後、ユーザープロフィールを取得します
  4. ログイン状態に応じて、異なるUIを表示します

ログイン後の専用ページに遷移するルーティングの設定

ログイン後は専用ページ(マイページなど)に遷移させるのが一般的です。React Routerを使った実装例です。※importパスは実際のシステム構成に合わせて変更してください。

App.jsx

import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
import liff from '@line/liff';
import LoginComponent from './components/LoginComponent';
import MyPage from './components/MyPage';
import Header from './components/Header';
import Footer from './components/Footer';
import './App.css';

function App() {
  const [isLiffInitialized, setIsLiffInitialized] = useState(false);
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(() => {
    // 環境変数からLIFF IDを取得
    const liffId = process.env.REACT_APP_LIFF_ID || import.meta.env.VITE_LIFF_ID;
    
    // LIFF初期化
    if (!isLiffInitialized) {
      liff.init({ liffId })
        .then(() => {
          console.log('LIFF初期化完了');
          setIsLiffInitialized(true);
          
          // ログイン状態を更新
          setIsLoggedIn(liff.isLoggedIn());
        })
        .catch((err) => {
          console.error('LIFF初期化エラー:', err.message);
        });
    }
  }, [isLiffInitialized]);

  // ログイン状態が変わったときに検知する
  useEffect(() => {
    const checkLoginStatus = () => {
      if (isLiffInitialized) {
        setIsLoggedIn(liff.isLoggedIn());
      }
    };

    window.addEventListener('focus', checkLoginStatus);
    return () => window.removeEventListener('focus', checkLoginStatus);
  }, [isLiffInitialized]);

  // ログイン必須のルートに対するガード
  const PrivateRoute = ({ children }) => {
    return isLoggedIn ? children : <Navigate to="/" />;
  };

  return (
    <Router>
      <div className="app-container">
        <Header isLoggedIn={isLoggedIn} />
        
        <main className="main-content">
          <Routes>
            {/* ログインページ(ホーム) */}
            <Route path="/" element={<LoginComponent />} />
            
            {/* ログイン後のマイページ(要認証) */}
            <Route
              path="/mypage"
              element={
                <PrivateRoute>
                  <MyPage />
                </PrivateRoute>
              }
            />
            
            {/* 他のルートを追加 */}
          </Routes>
        </main>
        
        <Footer />
      </div>
    </Router>
  );
}

export default App;

次に、マイページコンポーネントを作成します。このページはログイン後に表示され、ユーザー情報を活用して個別のコンテンツを提供できます。

MyPage.jsx

import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import liff from '@line/liff';
import './MyPage.css';

function MyPage() {
  const navigate = useNavigate();
  const [userProfile, setUserProfile] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  
  useEffect(() => {
    // ログイン状態チェック
    if (!liff.isLoggedIn()) {
      navigate('/');
      return;
    }
    
    // ユーザー情報取得
    const fetchUserInfo = async () => {
      try {
        const profile = await liff.getProfile();
        setUserProfile(profile);
        setIsLoading(false);
        
        // ここで必要に応じてバックエンドAPIにユーザー情報を送信
        // saveUserInfoToBackend(profile);
      } catch (error) {
        console.error('プロフィール取得エラー:', error);
        setIsLoading(false);
      }
    };
    
    fetchUserInfo();
  }, [navigate]);
  
  // バックエンドAPIにユーザー情報を送信する関数(実装例)
  const saveUserInfoToBackend = async (profile) => {
    try {
      // idTokenを取得(JWT形式のトークン)
      const idToken = liff.getIDToken();
      
      // バックエンドAPIにPOSTリクエスト
      const response = await fetch('https://your-api.example.com/auth/line', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          idToken,
          profile,
        }),
      });
      
      if (response.ok) {
        const data = await response.json();
        console.log('バックエンド連携成功:', data);
        // 必要に応じてセッション情報などをlocalStorageに保存
      } else {
        console.error('バックエンド連携エラー:', response.status);
      }
    } catch (error) {
      console.error('APIエラー:', error);
    }
  };
  
  // ログアウト処理
  const handleLogout = () => {
    liff.logout();
    navigate('/');
  };
  
  if (isLoading) {
    return <div className="loading">読み込み中...</div>;
  }
  
  return (
    <div className="mypage-container">
      <h1>マイページ</h1>
      {userProfile && (
        <div className="user-profile">
          <div className="profile-header">
            <img 
              src={userProfile.pictureUrl} 
              alt={userProfile.displayName} 
              className="profile-photo"
            />
            <div className="profile-info">
              <h2>{userProfile.displayName} さん</h2>
              <p>ユーザーID: {userProfile.userId.substr(0, 8)}...</p>
              <p>ステータスメッセージ: {userProfile.statusMessage || '設定なし'}</p>
            </div>
          </div>
          
          <div className="content-section">
            <h3>特典コンテンツ</h3>
            <div className="content-cards">
              <div className="content-card">
                <h4>会員限定情報</h4>
                <p>LINE会員のみ閲覧できるコンテンツです。</p>
              </div>
              <div className="content-card">
                <h4>お得なクーポン</h4>
                <p>LINE友だち限定のクーポンをご利用いただけます。</p>
              </div>
            </div>
          </div>
          
          <button onClick={handleLogout} className="logout-button">
            ログアウト
          </button>
        </div>
      )}
    </div>
  );
}

export default MyPage;

その他の最低限のReactコード一式

以下は、LINEログイン機能を実装するための最小限のコード構成です。これらのファイルを作成することで、基本的なLINEログイン機能を持つReactアプリケーションが完成します。

環境変数ファイル(.env): LIFF IDなどの設定を保存します

# Create React Appの場合
REACT_APP_LIFF_ID=your-liff-id-here

# Viteの場合
VITE_LIFF_ID=your-liff-id-here

CSSファイル(MyPage.css): マイページのスタイリングを定義します

.mypage-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.user-profile {
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
  margin-top: 20px;
}

.profile-header {
  display: flex;
  align-items: center;
  margin-bottom: 30px;
}

.profile-photo {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  object-fit: cover;
  margin-right: 20px;
}

.profile-info h2 {
  margin: 0 0 10px;
  font-size: 1.5rem;
  color: #333;
}

.profile-info p {
  margin: 5px 0;
  color: #666;
}

.content-section {
  margin: 30px 0;
}

.content-section h3 {
  border-bottom: 2px solid #06C755;
  padding-bottom: 10px;
  margin-bottom: 20px;
  color: #333;
}

.content-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
}

.content-card {
  background-color: #f9f9f9;
  border-radius: 8px;
  padding: 15px;
  transition: transform 0.3s ease;
}

.content-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.content-card h4 {
  color: #06C755;
  margin-top: 0;
}

.logout-button {
  background-color: #ff5252;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 10px 20px;
  font-size: 1rem;
  cursor: pointer;
  transition: background-color 0.3s;
  margin-top: 20px;
}

.logout-button:hover {
  background-color: #ff3232;
}

.loading {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 300px;
  font-size: 1.2rem;
  color: #666;
}

App.css: アプリケーション全体のスタイリングを定義します

.app-container {
  font-family: 'Helvetica Neue', Arial, sans-serif;
  line-height: 1.6;
  color: #333;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.main-content {
  flex: 1;
  padding: 20px;
  background-color: #f5f5f5;
}

/* ログインページスタイル */
.login-container {
  max-width: 400px;
  margin: 80px auto;
  padding: 30px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  text-align: center;
}

.login-container h2 {
  margin-bottom: 30px;
  color: #333;
}

.line-login-button {
  background-color: #06C755;
  color: white;
  border: none;
  border-radius: 4px;
  padding: 12px 24px;
  font-size: 1rem;
  font-weight: bold;
  cursor: pointer;
  transition: background-color 0.3s;
  width: 100%;
  max-width: 300px;
}

.line-login-button:hover {
  background-color: #05a849;
}

/* プロフィール表示スタイル */
.profile-container {
  max-width: 400px;
  margin: 40px auto;
  padding: 20px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  text-align: center;
}

.profile-image {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  object-fit: cover;
  margin: 0 auto 20px;
}

/* エラーメッセージ */
.error-message {
  color: #e74c3c;
  background-color: #fde2e2;
  padding: 15px;
  border-radius: 4px;
  margin: 20px auto;
  max-width: 600px;
  text-align: center;
}

Header.jsx: ヘッダーコンポーネントを作成します

import { Link } from 'react-router-dom';
import liff from '@line/liff';
import './Header.css';

function Header({ isLoggedIn }) {
  const handleLogout = () => {
    if (isLoggedIn) {
      liff.logout();
      window.location.href = '/';
    }
  };

  return (
    <header className="header">
      <div className="header-container">
        <Link to="/" className="logo">
          LINE Login Demo
        </Link>
        
        <nav className="nav">
          <ul className="nav-list">
            <li>
              <Link to="/" className="nav-link">ホーム</Link>
            </li>
            {isLoggedIn ? (
              <>
                <li>
                  <Link to="/mypage" className="nav-link">マイページ</Link>
                </li>
                <li>
                  <button onClick={handleLogout} className="nav-button logout">
                    ログアウト
                  </button>
                </li>
              </>
            ) : (
              <li>
                <Link to="/" className="nav-button login">
                  ログイン
                </Link>
              </li>
            )}
          </ul>
        </nav>
      </div>
    </header>
  );
}

export default Header;

Footer.jsx: フッターコンポーネントを作成します

import './Footer.css';

function Footer() {
  const currentYear = new Date().getFullYear();
  
  return (
    <footer className="footer">
      <div className="footer-container">
        <p>© {currentYear} LINE Login Demo App. All rights reserved.</p>
        <div className="footer-links">
          <a href="https://developers.line.biz/ja/docs/liff/" target="_blank" rel="noopener noreferrer">
            LIFF Documentation
          </a>
          <a href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
            React
          </a>
        </div>
      </div>
    </footer>
  );
}

export default Footer;

続きは、【コード公開】LIFEアプリ作成手順|LINEログイン機能を実装する方法③