# 如何在 LINE LIFF 中確保用戶把官方帳號加為好友

根據我之前寫的文章「如何重新看到 LINE LIFF v2 的授權畫面?(opens new window) 」裡面的測試,在 LIFF 使用 Bot Link 功能時,授權頁面中的「加入好友」選項預設不會勾選(這好像是需要額外付費才會有的功能),所幸,我們還是能用 LIFF SDK 來達成類似的需求。我特地寫了這篇文章來分享我最近在開發公司專案的活動中,如何在 LINE LIFF 中確保用戶把官方帳號加為好友。

# DEMO

這是我第一次實作這個機制,歡迎大家點下方「活動邀請連結」來試玩看看:

「微笑小學堂」活動

首先,為了模擬用戶只授權 LIFF 但是沒有加入官方帳號,所以我在授權頁面的第一頁按下「許可」,在第二頁的時候按下「取消」:

圖 1-1 圖 1-2

然後接下來就會開啟 LIFF 頁面,透過呼叫 LIFF 的 liff.getFriendship() 來得知用戶是否有加入好友,如果還沒加入,就要給用戶一個加入連結:

圖 1-3 圖 1-4

當用戶點選下方「加入好友」的連結之後,就會跳到官方的加入頁面,當用戶點選加入後,便會直接回到 LIFF 頁面中,當我們發現用戶已經成功加入好友後,就需要幫用戶切換到活動頁面:

圖 1-5 圖 1-6

官方 Bot Link 文件: https://developers.line.biz/en/docs/line-login/link-a-bot/(opens new window)

在 LIFF v2 中,官方提供了一個 Bot Link 功能,當你選擇啟用 ON 以後,有兩種模式可以選擇:

第一個是 normal 模式,也就是在 LIFF 的授權頁面中,會額外多一個「加入好友」或是「解除封鎖」的選項,但是這個選項預設不會勾選:

加入好友 解除封鎖

第二種是 aggressive 模式,這個模式會讓 LIFF 授權頁面變成兩步驟,第一步只有 LIFF 相關的權限,然後在第二步的時候,會有兩顆很明顯按鈕的讓用戶決定要不要加入好友:

1. 授權 LIFF 2. 加入好友

如果你的授權頁面中「加入好友」預設也是和我一樣不會勾選的話,建議選擇第二種模式,提高用戶加入好友的機會。

# 透過 JS 確保用戶把官方帳號加為好友

由於在 LIFF 的授權頁面之中,不管你選擇的是兩種模式中的哪一種,用戶都有可能會選擇不加為好友,所以我們可以在 LIFF 中用 JS 呼叫 LIFF 的 SDK 來確保用戶加入好友。

// import _ from 'lodash'
// * 需要使用到 lodash 函式庫 (別名 _ )
async function isFriend () {
  return _.get(await liff.getFriendship(), 'friendFlag', false)
}

如果我們發現用戶沒有加入好友時,我們需要在網頁上提供一個加入好友的連結給用戶,引導用戶加入好友:(以 @youbike 為例)

https://line.me/R/ti/p/{LINE ID}
https://line.me/R/ti/p/@youbike

如果你跟我一樣使用上面那個加入連結的話,你會發現用戶在手機上加入好友以後,會回到剛剛的 LIFF 頁面中,為了讓用戶在加入好友後,能夠繼續 LIFF 頁面該有的流程,所以我們會需要不停的檢查用戶是否加入好友:

async function waitAddFriend () {
  await loginPromise // 確保 LIFF 頁面初始化結束
  if (await isFriend()) return // 如果用戶已經是好友就直接結束
  showPage('follow') // 顯示加入好友的頁面,裡面要給用戶加入連結
  while (true) {
    // 避免過於頻繁執行 isFriend() 所以設定 0.5 秒的間隔
    await sleep(500)
    // 如果用戶已經是好友就結束
    if (await isFriend()) return
  }
}

接下來我來補充一下程式碼中的 loginPromise

在 LIFF 網頁剛載入時,會需要進行 LIFF SDK 的初始化,初始化完成後,我會直接要求用戶登入 LINE。我通常會習慣自己另外寫一個 loginPromise 來確保我接下來的程式都是在登入之後才執行。

在這個 Promise 中,有幾個需要特別注意的地方:

  1. 在 LIFF SDK 尚未處理 liff.state 參數前,我會透過一個不會結束的 Promise 避免程式繼續執行下去造成異常的執行結果。
  2. 在用戶尚未登入 LINE 的時候也是透過一個不會結束的 Promise 避免程式繼續執行下去
  3. 在宣告完 async 函式以後,就馬上執行 async 函式,並且把 Promise 保留下來
const loginPromise = (async () => {
  await liff.init({ liffId: '#{liffId}' })
  if (new URL(location).searchParams.get('liff.state') !== null) {
    // 由於 SDK 尚未處理 liff.state 的跳轉
    // 所以透過一個不會結束的 Promise 避免程式繼續執行下去
    await new Promise(resolve => {})
  }
  if (!liff.isLoggedIn()) {
    liff.login({ redirectUri: location.href })
    // 由於用戶尚未登入 LINE
    // 所以透過一個不會結束的 Promise 避免程式繼續執行下去
    await new Promise(resolve => {})
  }
  // ... 其他初始化程式碼 ...
})()

以下是比較完整的範例程式:

// import _ from 'lodash'
const loginPromise = (async () => {
  await liff.init({ liffId: '#{liffId}' })
  if (new URL(location).searchParams.get('liff.state') !== null) {
    // 由於 SDK 尚未處理 liff.state 的跳轉
    // 所以透過一個不會結束的 Promise 避免程式繼續執行下去
    await new Promise(resolve => {})
  }
  if (!liff.isLoggedIn()) {
    liff.login({ redirectUri: location.href })
    // 由於用戶尚未登入 LINE
    // 所以透過一個不會結束的 Promise 避免程式繼續執行下去
    await new Promise(resolve => {})
  }
  // ... 其他初始化程式碼 ...
})()

async function isFriend () {
  return _.get(await liff.getFriendship(), 'friendFlag', false)
}

async function sleep (t) {
  await new Promise(resolve => { setTimeout(resolve, t) })
}

async function waitAddFriend () {
  await loginPromise // 確保 LIFF 頁面初始化結束
  if (await isFriend()) return // 如果用戶已經是好友就直接結束
  showPage('follow') // 顯示加入好友的頁面,裡面要給用戶加入連結
  while (true) {
    // 避免過於頻繁執行 isFriend() 所以設定 0.5 秒的間隔
    await sleep(500)
    // 如果用戶已經是好友就結束
    if (await isFriend()) return
  }
}

async function init () {
  await waitAddFriend() // 等待用戶加入好友
  showPage('main') // 顯示主要頁面
}

# 相關連結

TIP

本文範例程式的原始碼授權為 MIT License。