できること
・個別のQRコードを発行し、友だちがフォローした際に、流入元の情報が友だちレコードに表示・記録される。
・LINEの友だち登録済みのメールアドレスを取得可能。
設定方法
① LIFFアプリを取得する
手順 https://developers.line.biz/ja/docs/liff/registering-liff-apps/
step01 LINEログイン選択

LINE ログインとLIFF アプリ基本設定
➁ LIFFのエンコードポイント用のSalesforce 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のメールアドレスが登録される。
・「認証済み」項目
チェックが付くように更新する。






