個別のQRコード発行から流入経路分析をする方法

個別のQRコードを発行することで簡単に流入経路分析を行う方法について解説します。

できること

・LINEのトークルーム内で機能するWebアプリのLIFFアプリを取得する
・LINEの友だち登録済みのメールアドレスを取得可能になる
・個別のQRコードを発行し、友だちがフォローした際に、流入元の情報が友だちレコードに表示・記録できる

設定方法

① LIFFアプリを取得する

手順 https://developers.line.biz/ja/docs/liff/registering-liff-apps/

step01 「LINEログイン」を選択

step02 「デモ LIFF アプリ」を選択して作成する

※エンドポイントURLは後で設定する
LIFF IDLIFF URLをメモする

LINE ログインとLIFF アプリ基本設定

➁ LIFFのエンドポイント用のSalesforce VFページを作成し公開する

設定⇒Visualforceページ⇒ FmlLineFiffPage のファイルからソースをコピーして新規VFページを作成する

上記作成したVFページはこちらを参照して公開する          
公開後、上記①のStep2にて作成したLIFFのエンドポイントURLに更新する

※設定例の場合、「liffpageB」といったVFページのAPI名となっている


※以下ソースコードから作成してもよい

VFページコード例:

<apex:page controller=”bfml.FmlLineWebHookCallback” cache=”false” showHeader=”false” sidebar=”false” standardStylesheets=”false” applyHtmlTag=”false” applyBodyTag=”false”> <apex:includeScript value=”https://static.line-scdn.net/liff/edge/2/sdk.js”/> <apex:includeScript value=”/soap/ajax/50.0/connection.js”/> <apex:includeScript value=”/soap/ajax/50.0/apex.js”/> <body> <div style=”padding-top:5%;font-size:25px;text-align:center”>友だち追加画面へ遷移…</div> <script> let param_liffid = ‘2007178752-3aGmWBwP’; //TODO liffId let param_redirect = ‘https://lin.ee/pLjH98I’; //TODO 友だち追加URL //公式アカウントチャネルID let param_channelid = ‘{!JSENCODE($CurrentPage.parameters.c)}’; //流入経路ID let srcfrom = ‘{!JSENCODE($CurrentPage.parameters.p)}’; //TODO パラメータ追加する場合には下に追加してください。 window.onload = function () { liff.init({ liffId: param_liffid }) .then(() => { let email ; // LIFFアプリが初期化された後に実行 if (liff.isLoggedIn()) { const idToken = liff.getDecodedIDToken(); if (idToken) { email = idToken.email; } } else { // ログインしていない場合はログイン処理 liff.login({ redirectUri: window.location.href }); } liff.getProfile() .then(profile => { let parameters = { bfml__Mail__c: email, bfml__SourceOfTraffic__c: srcfrom, Id: profile.userId, //友だち項目API名: 値, //その他カスタマイズ項目も追加可能 }; let fieldValues = { dealCommand: “line_member_update”, //固定 channelid: param_channelid, //LINEチャネルID dealUserKey: profile.userId, //LINE ID dealContent: JSON.stringify(parameters), }; bfml.FmlLineWebHookCallback.updateMemberInfo(JSON.stringify(fieldValues),function(result, event) { if (event.status) { // debug用メッセージ } else { // debug用メッセージ2 } }, {escape: false}); window.location= param_redirect; }) .catch((err) => { alert(“liff getProfile error : ” + err); }); }) .catch((err) => { alert(“liff init error : ” + err); }); } </script> </body> </apex:page>

コピー後の改修点:
① 前後の <!– –> コメントを削除する
➁ let param_liffid = ‘2003872552-DrXXXXXX‘; //TODO liffId → 上記のLIFFで置換
let param_redirect = ‘https://lin.ee/xR5XXXX‘; //TODO 友だち追加URL → 御社の公式アカウントのURLで置換 ※友だち追加URLはこちら以下の部分から取得する

改良版(見た目、エラー処理強化):

<apex:page controller="bfml.FmlLineWebHookCallback"  cache="false" showHeader="false"  sidebar="false" standardStylesheets="false"  applyHtmlTag="false" applyBodyTag="false">
    
    <apex:includeScript value="https://static.line-scdn.net/liff/edge/2/sdk.js"/>
    <apex:includeScript value="/soap/ajax/50.0/connection.js"/>
    <apex:includeScript value="/soap/ajax/50.0/apex.js"/>
    
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
        <style>
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }
            
            body {
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Hiragino Sans', sans-serif;
                background: linear-gradient(135deg, #00B900 0%, #00C300 100%);
                min-height: 100vh;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            
            .container {
                text-align: center;
                padding: 2rem;
                background: white;
                border-radius: 16px;
                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
                max-width: 90%;
                width: 400px;
            }
            
            .loading-spinner {
                width: 50px;
                height: 50px;
                border: 4px solid #f3f3f3;
                border-top: 4px solid #00B900;
                border-radius: 50%;
                animation: spin 1s linear infinite;
                margin: 0 auto 1.5rem;
            }
            
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
            
            .message {
                font-size: 1.25rem;
                color: #333;
                margin-bottom: 1rem;
            }
            
            .error-message {
                color: #d32f2f;
                font-size: 0.9rem;
                margin-top: 1rem;
                padding: 1rem;
                background: #ffebee;
                border-radius: 8px;
                display: none;
            }
            
            .debug-info {
                margin-top: 1rem;
                padding: 1rem;
                background: #f5f5f5;
                border-radius: 8px;
                font-size: 0.8rem;
                text-align: left;
                max-height: 200px;
                overflow-y: auto;
                display: none;
            }
            
            .line-icon {
                font-size: 3rem;
                margin-bottom: 1rem;
            }
        </style>
    </head>
    
    <body>
        <div class="container">
            <div class="line-icon">📱</div>
            <div class="loading-spinner"></div>
            <div class="message" id="statusMessage">友だち追加画面へ遷移中...</div>
            <div class="error-message" id="errorMessage"></div>
            <div class="debug-info" id="debugInfo"></div>
        </div>
        
        <script>
            (function() {
                'use strict';
                
                // ==================== 設定 ====================
                // ※ 本番環境ではCustom SettingsまたはCustom Metadataから取得することを推奨
                const CONFIG = {
     		    LIFF_ID: '2006873165-D2w7BwN6',
                    FRIEND_ADD_URL: 'https://lin.ee/VUOubDI',
                    DEAL_COMMAND: 'line_member_update',
                    DEBUG_MODE: false // デバッグモード (本番ではfalse)
                };
                
                // ==================== DOM要素 ====================
                const elements = {
                    statusMessage: document.getElementById('statusMessage'),
                    errorMessage: document.getElementById('errorMessage'),
                    debugInfo: document.getElementById('debugInfo')
                };
                
                // ==================== ユーティリティ関数 ====================
                
                /**
                 * デバッグ情報を表示
                 */
                function debugLog(label, data) {
                    console.log(`[DEBUG] ${label}:`, data);
                    
                    if (CONFIG.DEBUG_MODE) {
                        elements.debugInfo.style.display = 'block';
                        const debugText = `${label}:\n${JSON.stringify(data, null, 2)}\n\n`;
                        elements.debugInfo.textContent += debugText;
                    }
                }
                
                /**
                 * エラーメッセージを表示
                 */
                function showError(message, technicalDetails = '') {
                    elements.statusMessage.textContent = 'エラーが発生しました';
                    elements.errorMessage.textContent = message;
                    elements.errorMessage.style.display = 'block';
                    
                    console.error('Error:', message, technicalDetails);
                    debugLog('Error Details', { message, technicalDetails });
                }
                
                /**
                 * ステータスメッセージを更新
                 */
                function updateStatus(message) {
                    elements.statusMessage.textContent = message;
                    debugLog('Status Update', message);
                }
                
                /**
                 * URLパラメータをパースして取得
                 * liff.state または通常のクエリパラメータから取得
                 */
                function getParameters() {
                    const params = {
                        channelId: null,
                        sourceFrom: null
                    };
                    
                    try {
                        // 1. 現在のURLからパラメータを取得
                        const urlParams = new URLSearchParams(window.location.search);
                        debugLog('URL Search Params', Object.fromEntries(urlParams));
                        
                        // 2. liff.stateパラメータが存在する場合(LINEログイン後)
                        const liffState = urlParams.get('liff.state');
                        
                        if (liffState) {
                            debugLog('liff.state detected', liffState);
                            
                            // liff.stateをデコード
                            // 例: ?p=a07IS00000563BCYAY&c=2006850912
                            const decodedState = decodeURIComponent(liffState);
                            debugLog('Decoded liff.state', decodedState);
                            
                            // liff.state内のパラメータをパース
                            const stateParams = new URLSearchParams(decodedState);
                            params.channelId = stateParams.get('c');
                            params.sourceFrom = stateParams.get('p');
                            
                            debugLog('Params from liff.state', params);
                        } else {
                            // 3. 通常のクエリパラメータから取得(初回アクセス時)
                            params.channelId = urlParams.get('c');
                            params.sourceFrom = urlParams.get('p');
                            
                            debugLog('Params from URL', params);
                        }
                        
                        // 4. Visualforceパラメータからフォールバック
                        if (!params.channelId) {
                            params.channelId = '{!JSENCODE($CurrentPage.parameters.c)}' || null;
                        }
                        if (!params.sourceFrom) {
                            params.sourceFrom = '{!JSENCODE($CurrentPage.parameters.p)}' || null;
                        }
                        
                        debugLog('Final Parameters', params);
                        
                    } catch (err) {
                        console.error('パラメータ取得エラー:', err);
                        debugLog('Parameter Parsing Error', err);
                    }
                    
                    return params;
                }
                
                /**
                 * パラメータバリデーション
                 */
                function validateParameters(params) {
                    const errors = [];
                    
                    if (!params.channelId) {
                        errors.push('チャネルID(c)が指定されていません');
                    }
                    
                    if (!CONFIG.LIFF_ID) {
                        errors.push('LIFF IDが設定されていません');
                    }
                    
                    if (!CONFIG.FRIEND_ADD_URL) {
                        errors.push('友だち追加URLが設定されていません');
                    }
                    
                    if (errors.length > 0) {
                        throw new Error(errors.join('\n'));
                    }
                    
                    debugLog('Validation Passed', params);
                }
                
                /**
                 * 会員情報を更新
                 */
                function updateMemberInfo(profile, email, params) {
                    return new Promise((resolve, reject) => {
                        const memberData = {
                            bfml__Mail__c: email || null,
                            bfml__SourceOfTraffic__c: params.sourceFrom || null,
                            Id: profile.userId
                            // カスタム項目を追加する場合はここに記述
                            // CustomField__c: value
                        };
                        
                        const fieldValues = {
                            dealCommand: CONFIG.DEAL_COMMAND,
                            channelid: params.channelId,
                            dealUserKey: profile.userId,
                            dealContent: JSON.stringify(memberData)
                        };
                        
                        debugLog('Update Member Info Request', {
                            memberData,
                            fieldValues
                        });
                        
                        bfml.FmlLineWebHookCallback.updateMemberInfo(
                            JSON.stringify(fieldValues),
                            function(result, event) {
                                if (event.status) {
                                    debugLog('Update Member Info Success', result);
                                    resolve(result);
                                } else {
                                    console.error('会員情報更新失敗:', event.message);
                                    debugLog('Update Member Info Error', event);
                                    reject(new Error(event.message));
                                }
                            },
                            { escape: false }
                        );
                    });
                }
                
                /**
                 * LINEプロフィール取得とメールアドレス取得
                 */
                async function getUserInfo() {
                    let email = null;
                    
                    // メールアドレス取得(IDトークンから)
                    if (liff.isLoggedIn()) {
                        try {
                            const idToken = liff.getDecodedIDToken();
                            debugLog('ID Token', idToken);
                            
                            if (idToken && idToken.email) {
                                email = idToken.email;
                                console.log('メールアドレス取得成功:', email);
                            }
                        } catch (err) {
                            console.warn('メールアドレス取得失敗:', err);
                            debugLog('Email Fetch Error', err);
                            // メールアドレスが取得できなくても処理は続行
                        }
                    }
                    
                    // プロフィール取得
                    const profile = await liff.getProfile();
                    debugLog('LINE Profile', profile);
                    console.log('プロフィール取得成功:', profile.displayName);
                    
                    return { profile, email };
                }
                
                /**
                 * 友だち追加ページへリダイレクト
                 */
                function redirectToFriendAdd() {
                    updateStatus('友だち追加ページへ移動します...');
                    setTimeout(() => {
                        debugLog('Redirecting to', CONFIG.FRIEND_ADD_URL);
                        window.location.href = CONFIG.FRIEND_ADD_URL;
                    }, 500);
                }
                
                // ==================== メイン処理 ====================
                
                /**
                 * 初期化処理
                 */
                async function initialize() {
                    try {
                        debugLog('Initialize Start', {
                            url: window.location.href,
                            config: CONFIG
                        });
                        
                        // 1. パラメータ取得
                        const params = getParameters();
                        
                        // 2. パラメータバリデーション
                        validateParameters(params);
                        
                        // 3. LIFF初期化
                        updateStatus('LINE認証中...');
                        await liff.init({ liffId: CONFIG.LIFF_ID });
                        debugLog('LIFF Initialized', {
                            isLoggedIn: liff.isLoggedIn(),
                            isInClient: liff.isInClient()
                        });
                        
                        // 4. ログイン確認
                        if (!liff.isLoggedIn()) {
                            debugLog('Not Logged In', 'Redirecting to login');
                            
                            // パラメータを保持してログインにリダイレクト
                            // liff.login時に現在のURLがliff.stateとして保持される
                            liff.login({ redirectUri: window.location.href });
                            return; // ログイン後リダイレクトされるため処理終了
                        }
                        
                        // 5. ユーザー情報取得
                        updateStatus('ユーザー情報取得中...');
                        const { profile, email } = await getUserInfo();
                        
                        // 6. Salesforce会員情報更新
                        updateStatus('会員情報更新中...');
                        await updateMemberInfo(profile, email, params);
                        
                        // 7. 友だち追加ページへリダイレクト
                        redirectToFriendAdd();
                        
                    } catch (err) {
                        // エラー種別に応じたメッセージ
                        let userMessage = '処理中にエラーが発生しました。';
                        
                        if (err.message.includes('LIFF')) {
                            userMessage = 'LINE認証に失敗しました。再度お試しください。';
                        } else if (err.message.includes('チャネルID')) {
                            userMessage = '設定エラーが発生しました。URLパラメータを確認してください。\n\n必要なパラメータ:\n・c: チャネルID\n・p: 流入経路ID (任意)';
                        } else if (err.message.includes('プロフィール')) {
                            userMessage = 'ユーザー情報の取得に失敗しました。';
                        } else if (err.message.includes('会員情報更新')) {
                            userMessage = '会員情報の更新に失敗しました。しばらくしてから再度お試しください。';
                        }
                        
                        showError(userMessage, err);
                    }
                }
                
                // ==================== エントリーポイント ====================
                
                // ページ読み込み完了時に実行
                if (document.readyState === 'loading') {
                    document.addEventListener('DOMContentLoaded', initialize);
                } else {
                    initialize();
                }
                
            })();
        </script>
    </body>
</apex:page>

③ DX-LINEにてQRコードを作成する

ホーム→設定・登録→すべて表示→「流入経路」を 新規作成する

下の図のように登録後に再度編集⇒保存を行うと、「QRCodeダウンロード」が作成可能
※LIFF URLは上記①のstep2で取得したURLを入力

④ 動作確認

上記作成した「 QRCodeダウンロード」をクリックし、表示したQRコードから友だちフォローをもらうと、友だちレコードの以下の項目に値がセットされる

・「流入経路」項目
 項目が表示されなかった場合、ページレイアウトを変更すれば表示されるようになる

・「認証済み」項目
 チェックが付くように更新

・「メールアドレス」項目
 LINEのメールアドレスが登録される

これで、流入経路、メールアドレス、認証済みの設定ができ、分析が可能です。