[機能]流入経路分析

できること

・個別のQRコードを発行し、友だちがフォローした際に、流入元の情報が友だちレコードに表示・記録される。
・LINEの友だち登録済みのメールアドレスを取得可能。

設定方法

① LIFFアプリを取得する

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

step01 LINEログイン選択

step02 LIFF 選択し作成する
※エンドポイントURLは後で設定する。
LIFFIDとLIFF URLメモする。

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

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

設定⇒Visualforceページ⇒ FmlLineFiffPage のファイルからソースをコピーして新規VFページを作成する。
※以下ソースコードから作成してもよい

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>


上記作成したVFページこちら参照して公開する。
公開後、上記①のStep2にて作成したLIFFのエンコードポイントURLに更新する。
※設定例:「liffpageB」VFページのAPI名

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

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

LIFF URLは上記①のstep2で取得したURL。

保存後に、QRコードのダウンロードできます。

④ 動作確認


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

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

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

認証済み」項目
 チェックが付くように更新する。

タイトルとURLをコピーしました