返回開發人員最新消息

使用 WhatsApp Flows 預約:建立 Node.js 後端

2024年2月27日發佈者:Gafi G & Iryna Wagner

您可以運用 WhatsApp Flows 建立互動式訊息,讓用戶直接在 WhatsApp 上完成操作。Flows 可讓您針對用戶互動建立畫面。例如,您可以建立簡單的輸入表單來蒐集潛在顧客或提交評論。此外,您可以設計多個畫面的複雜流程來安排預約。

本指南將引導您建立一個可讓用戶透過 WhatsApp Flows 預約的 Node.js 應用程式。您將在 WhatsApp Business 平台上建立流程,然後設定 Webhook 以接收流程的回應並預約。

必要條件

若要繼續遵循此教學導覽來操作,請確定您具備以下條件:

建立 WhatsApp 流程

建立 WhatsApp Flow 有兩種方法:Flows 建立工具(可透過 WhatsApp 管理工具存取)和 Flows API。本教學使用 Flows 建立工具。

建立流程

在 WhatsApp 管理工具主控板左側功能表中,選擇帳號工具。然後點擊流程

WhatsApp 管理工具圖像

點擊右上角的建立流程

建立流程圖像

在出現的對話方塊中,填寫預約流程的詳細資訊:

  • 名稱 — 輸入 BookAppointment,或選擇您喜歡的其他名稱。
  • 類別 — 選擇預約
  • 範本 — 選擇預約。您將會使用該範本,因為其中包含了預約的必要元素。這些元素包括預約詳細資料、用戶詳細資料輸入、預約摘要和公司條款顯示的畫面。您可以進一步自訂範本以符合您的使用情況。
預約圖像

點擊提交即可建立流程。

您可以在建立工具用戶介面右側預覽流程。預約畫面可以讓用戶選擇預約詳細資料,例如地點和日期。詳細資料畫面是用戶輸入資訊的地方,摘要畫面會顯示預約摘要最後一個畫面顯示公司的條款。

編輯流程時,流程仍維持在草稿狀態。目前您可將流程分享給團隊成員,但只能用於測試用途。若要將流程分享給大量受眾,您需要發佈流程。但是,流程發佈後將無法編輯。由於您仍需要為此預約流程新增端點網址,因此現在可先保留為草稿,然後繼續下一個步驟,您將會在該步驟中設定端點。

設定流程的端點

WhatsApp Flows 可讓您連結到外部端點。外部端點可以為您的流程和控制路由提供動態資料,還可以接收用戶從流程提交的回覆。

基於測試目的,本文會使用 Glitch 來代管端點。Glitch 是選用選項,而且不需要使用 Flows。您可以複製 GitHub 中的端點程式碼,並在您偏好的任何環境中執行。

在 Glitch 中存取端點(endpoint)程式碼,然後編譯(remix)以獲得不重複的網域。若要重新編譯,可點擊頁面頂端的 Remix。專屬網域將會以預留位置形式出現在 Glitch 頁面右側的輸入元素中。

在繼續之前,讓我們先瀏覽一下程式碼。src 目錄中有四個 JavaScript 檔案:encryption.jsflow.jskeyGenerator.jsserver.js。入口檔案是 server.js,我們先來看看。

server.js

server.js 檔案一開始會透過設定 Express 應用程式來使用 express.json 中介軟體,以剖析傳入的 JSON 要求。接著,它會載入端點所需的環境變數。

const { APP_SECRET, PRIVATE_KEY, PASSPHRASE, PORT = "3000" } = process.env;

APP_SECRET 會用於簽署驗證,可協助您檢查訊息是否是透過 WhatsApp 傳入,以判斷是否可以安全地處理。您需要將它新增到 .env 檔案。

若要存取您的 APP_SECRET,請在 Meta for Developers 上前往您應用程式上的主控板。在左側導覽面板中的應用程式設定下方,選擇基本。點擊應用程式密鑰下方的顯示並複製密鑰。然後,返回 Glitch,開啟 .env 檔案並搭配複製的密鑰值建立一個名為 APP_SECRET 的變數。

PRIVATE_KEY 可協助解密收到的訊息。PASSPHRASE 將用於驗證私密金鑰。除了私密金鑰之外,您也需要相對應的公開金鑰,您稍後將會上傳該公開金鑰。切勿在此使用生產帳號的私密金鑰。建立用於在 Glitch 上測試的臨時私密金鑰,之後再將此金鑰更換成您自有基礎架構中的生產金鑰。

透過在 Glitch 終端機中執行以下命令來產生公開/私密金鑰組。將 <your-passphrase> 替換為您指定的密碼。透過點擊頁面底部的 TERMINAL 頁籤來存取 Glitch 終端機。

node src/keyGenerator.js <your-passphrase>

複製密碼和私密金鑰,並貼到 .env 檔案中。點擊左側邊欄上標記為 .env 的檔案,然後點擊頂端的 ✏️ Plain text請勿直接從用戶介面編輯金鑰,因為這會破壞您的金鑰格式

設定環境變數後,複製產生的公開金鑰並透過圖形 API 上傳公開金鑰

server.js 檔案也包含會執行不同步驟的 POST 端點:

  • 檢查私密金鑰是否存在:
       if (!PRIVATE_KEY) { throw new Error('Private key is empty.Please check your env variable "PRIVATE_KEY".'); }
  • 使用檔案底部的 isRequestSignatureValid 函式驗證要求簽章:
if(!isRequestSignatureValid(req)) { // Return status code 432 if request signature does not match. // To learn more about return error codes visit: https://developers.facebook.com/docs/whatsapp/flows/reference/error-codes#endpoint_error_codes return res.status(432).send(); }
  • 使用 encryption.js 檔案中的 decryptRequest 函式解密傳入訊息:
      let decryptedRequest = null; try { decryptedRequest = decryptRequest(req.body, PRIVATE_KEY, PASSPHRASE); } catch (err) { console.error(err); if (err instanceof FlowEndpointException) { return res.status(err.statusCode).send(); } return res.status(500).send(); } const { aesKeyBuffer, initialVectorBuffer, decryptedBody } = decryptedRequest; console.log("💬 Decrypted Request:", decryptedBody);
  • 決定要向用戶顯示什麼樣的流程畫面。稍後您將詳細瞭解 getNextScreen 函式。

const screenResponse = await getNextScreen(decryptedBody);

       console.log("👉 Response to Encrypt:", screenResponse);
  • 加密要傳送給用戶的回覆:
res.send(encryptResponse(screenResponse, aesKeyBuffer, initialVectorBuffer));

encryption.js

此檔案包含基於安全考量而用於將交換的訊息加密和解密的邏輯。本教學導覽不會著重於介紹本檔案的運作原理。

keyGenerator.js

此檔案可協助產生私密金鑰和公開金鑰,如您先前所見。與 encryption.js 檔案一樣,本教學導覽不會詳細地探討 keyGenerator.js 檔案。

flow.js

處理流程的邏輯位於此檔案中。它以一個名為 SCREEN_RESPONSES 的物件作為開頭。該物件包含畫面編號及其相對應的詳細資料,例如資料交換中使用的預設資料。此物件是從 Flow 建立工具的 "..." > 端點 > 片段 > 回覆產生的。在同一個物件中,您也會有另一個編號 SUCCESS,當流程成功完成時,該編號會傳回給客戶裝置。這會關閉流程。

getNextScreen 函式包含指引端點向用戶顯示哪些流程資料的邏輯。該函式會透過從解密的訊息中擷取必要資料來開始執行作業。

const { screen, data, version, action, flow_token } = decryptedBody;

WhatsApp Flows 端點通常會收到三個要求:

您可以在端點文件中找到各要求的詳細資料。

此函式會使用 if 陳述式處理系統健康檢查和錯誤通知並據以回應,如下方程式碼片段所示:

// handle health check request if (action === "ping") { return { version, data: { status: "active", }, }; } // handle error notification if (data?.error) { console.warn("Received client error:", data); return { version, data: { acknowledged: true, }, }; }
        

當用戶點擊流程的行動呼籲(CTA)按鈕時,會觸發 INIT 動作。此動作會返回預約畫面並顯示相關資料。這也會停用地點、日期和時間下拉式功能表,以確保用戶填寫所有欄位。

例如,只有在填入「地點」下拉式功能表時才會啟用「日期」下拉式功能表。收到 data_exchange 要求時,系統會處理這些欄位的啟用和停用作業。

// handle initial request when opening the flow and display APPOINTMENT screen if (action === "INIT") { return { ...SCREEN_RESPONSES.APPOINTMENT, data: { ...SCREEN_RESPONSES.APPOINTMENT.data, // these fields are disabled initially.Each field is enabled when previous fields are selected is_location_enabled: false, is_date_enabled: false, is_time_enabled: false, }, }; }

對於 data_exchange 動作,會使用 switch case 結構來根據畫面編號判斷要傳回哪些資料。如果畫面編號為 APPOINTMENT,將只會在選擇了前面的下拉式功能表時才會啟用後續的下拉式功能表欄位。

// Each field is enabled only when previous fields are selected is_location_enabled: Boolean(data.department), is_date_enabled: Boolean(data.department) && Boolean(data.location), is_time_enabled: Boolean(data.department) && Boolean(data.location) && Boolean(data.date)

對於 DETAILS 畫面,資料物件屬性的標題(例如地點和部門)會從 SCREEN_RESPONSES.APPOINTMENT.data 物件中擷取。這段程式碼會假設有一個有效的相符物件,所以請注意,如果找不到相符物件,可能會顯示錯誤。

現在,以地點物件的實例為例。選擇特定地點物件的動作是透過將陣列中物件的 id 屬性與 data.location 的值比對。

const departmentName = SCREEN_RESPONSES.APPOINTMENT.data.department.find( (dept) => dept.id === data.department ).title; const locationName = SCREEN_RESPONSES.APPOINTMENT.data.location.find( (loc) => loc.id === data.location ).title; const dateName = SCREEN_RESPONSES.APPOINTMENT.data.date.find( (date) => date.id === data.date

).title;

然後這些值會串連起來並在回應中傳回以顯示 SUMMARY 畫面。

const appointment = `${departmentName} at ${locationName} ${dateName} at ${data.time}`; const details = `Name: ${data.name} Email: ${data.email} Phone: ${data.phone} "${data.more_details}"`; return { ...SCREEN_RESPONSES.SUMMARY, data: { appointment, details, // return the same fields sent from client back to submit in the next step ...data, }, };
        

從客戶端提交 SUMMARY 畫面後,成功的回應會傳送至客戶裝置以將流程標記為完成。flow_token 是您在將流程傳送給用戶時可以設定的不重複識別碼。

// send success response to complete and close the flow return { ...SCREEN_RESPONSES.SUCCESS, data: { extension_message_response: { params: { flow_token, }, }, }, };

TERMS 畫面沒有需要交換的資料,因此端點不會處理它。

將端點新增到流程

在 Glitch 頁面的右上方,您可以透過點擊 kebab 選單圖示並選擇複製連結來複製網址。您也可以點擊右上方的分享來取得連結。

前往流程編輯工具。點擊編輯工具頂端顯示的棕色橫幅中的設定

此時會顯示彈出視窗,讓您可以在 Meta for Developers 上設定端點 URI、商家電話號碼和應用程式。進行必要設定後,請執行系統健康檢查。首先,執行互動式預覽並確保在互動式預覽設定中的在第一個畫面要求資料下方選擇要求資料。這會向端點傳送要求以擷取第一個畫面的資料,以驗證端點是否可用和您是否已實作系統健康檢查。

然後,透過點擊三個點(...)功能表並選擇發佈來發佈流程。這將會使用 action === "ping" 向您的端點傳送系統健康檢查要求,以在發佈之前先驗證端點是否已設定。

端點圖像

測試流程

完成設定後,在 WhatsApp 建立工具用戶介面中再次切換互動式預覽以測試流程。在顯示的彈出視窗中,選擇電話號碼,然後選擇在第一個畫面上要求資料下方的要求資料選項。透過點擊 X 圖示來關閉流程,以重新從行動呼籲按鈕開始測試流程。

點擊 LOGS 頁籤來開啟 Glitch 記錄。點擊清除來清除記錄。接著,回到 WhatsApp 建立工具用戶介面預覽。點擊預覽流程。您將會看到如下顯示的內容:

預覽流程圖像

現在,回到 Glitch 記錄。您將在解密的要求下方看到 INIT 動作、流程權杖和其他詳細資料。選擇部門下拉式功能表後,也會有要加密的回應傳回給用戶流程。

解密要求圖像

繼續以選擇部門。請注意 is_location_enabled 是如何設定為 true,以及動作如何已變更為 data_exchange

data_exchange 圖像

繼續嘗試執行流程並觀察 Glitch 記錄中的資料變化。當用戶透過行動裝置與流程互動時,也將會產生類似的記錄。

在下一節中,您將會建立一個會在用戶預約時向用戶傳送確認訊息的 Webhook。

設定 Webhook

當用戶完成流程時,系統會傳送標記流程已完成的訊息給訂閱的 Webhook。您將會從此 Webhook,透過聊天室中的訊息通知用戶預約成功。與端點類似,您也將會使用 Glitch 來測試。您可以前往此處存取程式碼並編譯。

Glitch 是選用選項,不需要使用 Flows。您可以複製 GitHub 中的 webhook 程式碼,並在您偏好的任何環境中執行。

設定環境變數

若要設定環境變數,請在 Glitch 上開啟 .env 檔案。將 VERIFY_TOKEN 設定為您偏好的任何字串,將 FLOW_ID 設定為您的流程編號,以及將 GRAPH_API_TOKEN 設定為您 WhatsApp Business 帳號的存取權杖。當您點擊左側導覽面板上 WhatsApp 區塊下方的 API 設定 時,您可以從 Meta for Developers 上應用程式的主控板取得存取權杖。

API 設定圖像

在顯示的頁面上,點擊暫時存取權杖卡片下方的複製按鈕。將金鑰貼到 .env 檔案中。

訂閱 Meta 主控板上的 Webhook

在您於 Meta for Developers 上的帳號中,點擊左側導覽面板中 WhatsApp 下方的設定功能表。

設定圖像

Webhook 頁籤卡中,點擊編輯。在開啟的對話方塊中,貼上複製的 Glitch 網址,並將 /webhook 附加到回呼網址欄位中。在驗證權杖欄位中,新增來自 .env 檔案中 TOKEN 變數的權杖。完成時點擊驗證並儲存。系統將會關閉對話方塊並回到主畫面。點擊管理並檢查 messages 欄位。您的 webhook 就大功告成了。

Webhook 程式碼逐步說明

該程式碼包含兩個路徑:POST /webhookGET /webhookGET 路徑會根據預先定義的驗證權杖檢查提供的權杖,並使用適當的狀態程式碼和挑戰權杖來回應,來處理 Webhook 驗證要求。

const verify_token = process.env.VERIFY_TOKEN; // Parse params from the webhook verification request let mode = req.query["hub.mode"]; let token = req.query["hub.verify_token"]; let challenge = req.query["hub.challenge"]; if (mode && token) { if (mode === "subscribe" && token === verify_token) { console.log("WEBHOOK_VERIFIED"); res.status(200).send(challenge); } else { res.sendStatus(403); } }

POST /webhook 路徑會處理傳入的 Webhook 通知。Webhook 要求可以有不同的裝載。因此,下方的程式碼會透過安全地存取要求欄位來讀取訊息和商家電話號碼,以防它們未定義。

const message = req.body.entry?.[0]?.changes[0]?.value?.messages?.[0]; const business_phone_number_id =

req.body.entry?.[0].changes?.[0].value?.metadata?.phone_number_id;

然後,程式碼會檢查傳入的要求是否是包含「預約」字詞的 "text" 類型訊息。如果訊息包含此字詞,流程會傳送給用戶。流程訊息會與 flow_action: "data_exchange,"一起傳送,這表示流程在啟動時將會向端點發出 INIT 要求以取得初始畫面和資料。

if ( message.type === "text" && // for demo purposes, send the flow message whenever a user sends a message containing "appointment" message.text.body.toLowerCase().includes("appointment") ) { // send flow message as per the docs here https://developers.facebook.com/docs/whatsapp/flows/gettingstarted/sendingaflow#interactive-message-parameters await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, type: "interactive", interactive: { type: "flow", header: { type: "text", text: "Hello there 👋", }, body: { text: "Ready to transform your space?Schedule a personalized consultation with our expert team!", }, footer: { text: "Click the button below to proceed", }, action: { name: "flow", parameters: { flow_id: FLOW_ID, flow_message_version: "3", // replace flow_token with a unique identifier for this flow message to track it in your endpoint & webhook flow_token: "<FLOW_TOKEN_PLACEHOLDER>", flow_cta: "Book an appointment", flow_action: "data_exchange", }, }, }, }, }); } ...

如果傳入訊息類型不是 "text",程式碼會檢查訊息類型是否為 "interactive"。互動類型 "nfm_reply" 表示傳入訊息是流程回應。然後,會向用戶傳回一則「您已成功預約」訊息。

... if ( message.type === "interactive" && message.interactive?.type === "nfm_reply" ) { // send confirmation message await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, text: { body: "You've successfully booked an appointment" }, }, }); } ...

接著,會將傳入的訊息標記為已讀,用戶就會看到藍色勾號。

... // mark incoming message as read await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", status: "read", message_id: message.id, }, }); ...
        

用戶體驗

在本範例中,用戶會向您的電話號碼傳送包含「預約」字詞的訊息,然後接收流程訊息。您也可以選擇在不同的互動後傳送流程或作為訊息範本。

用戶將會收到一則包含用於預約的行動呼籲按鈕的流程訊息,他們可以在其中填寫詳細資料。然後,當他們完成流程時,將會收到一則確認訊息。

向用戶傳送流程圖像

在本指南中,您已學會如何設定 WhatsApp 流程以實現流暢的預約安排。您已經使用 Flow 建立工具用戶介面製作了一份表單來蒐集用戶的預約詳細資料。

流程無需將用戶重新引導至外部網站進行預約,因ˇ此提升了顧客體驗。簡單直接的流程讓用戶能夠直接在 WhatsApp 內完成預訂。除了安排預約之外,您還可以使用 WhatsApp Flows 蒐集顧客服務意見回饋,或協助用戶註冊促銷活動或郵寄清單。WhatsApp Flows 也可以彈性地與外部 API 或您端點中的其他應用程式連結。

您可以使用 Flow 建立工具用戶介面輕鬆地建立 WhatsApp Flows。但是,您也可以使用流程 API 以程式設計方式建立流程。如需更多資訊,請參閱 WhatsApp Flows 文件