こんにちは、もっちゃんと申します。
この記事はMicrosoft Power Apps Advent Calendar 2022 カレンダー2の24日目です。
Microsoftのテクノロジーが今、とても熱いです!
Microsoftは巨大で優れた製品群を複数有しており、世界でも類を見ない状態となっています。
古くからある製品群はもちろんのこと、Microsoft Azureのクラウドを始め、GitHub, VS Code, OS, ハードウェアに至るまで。
さらにそれらをしっかり連携させてシナジーを生み出していく構想まであります。とても熱い展開が期待できそうですね^^
この記事ではそんな熱いテクノロジーのうち、Power Pagesを使って誰でも簡単にWebアプリが作れるところを見ていきますね。Webアプリを作成するにあたり、LINEが提供しているLINE Front-end Framework (略してLIFF) というWebアプリケーションに組み込める仕組みを利用すると便利なのでこちらも組み込んでいきますね!
Microsoft Power Platform の Power Pagesとは
セキュアなビジネス Webサイトをローコードで簡単に作成する
元はPower Apps ポータル というAppsの一員でしたが、Power Pagesとして独立した形でよりパワーアップして提供されることになりました。
Power Appsの画面上でもPower Pagesに誘導される感じになっていますね。
先日、サティア ナデラ氏が来日して行われたイベントの中で Do more with less (より少ないリソースでより多くのことを実現)
というメッセージが語られていました。
Power Pagesはまさにそういったことを実現できる優れたツール・サービスの1つと言えます。
これまで限られた専門家たちだけの領域だった開発の場が一変し、より多くの方が参入できる状況をMicrosoftが作り出しつつあります。
作り方
構成について
今回、試行錯誤した結果下記のような構成となりました。
フロント側の領域でPower Pages(Power Apps)とLINE(LIFF)が活躍し、バックエンド的なところやデータのやり取りにPower Automateを介するイメージです。
LINE DevelopersでMessaging API チャネルを作成
下記を参考に作成します。
https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-console
https://developers.line.biz/ja/docs/messaging-api/building-bot/#page-title
LINE DevelopersでLINE ログインチャネルの作成
下記を参考に作成します。
https://developers.line.biz/ja/docs/line-login/getting-started/#step-1-create-channel
https://developers.line.biz/ja/docs/liff/registering-liff-apps/#registering-liff-app
Power Pagesの作成
アプリの作成
下記サイトにアクセスします。
https://make.powerpages.microsoft.com/
名前とアドレスを入力し、「作成」をクリックします。
アドレスをコピーします。
LINE ログインチャネルの画面に戻り、LIFFのエンドポイントURLに先ほどのアドレスを追加します。
https://{アドレス}
画面をデザインします。
下記のように、テキストやボタンを追加して、作成します。
GUIではできない設定を以下でコードを書いていきます。
もともとのコードに下記を追加したコードを加えます。
・SDKのインポート
・テキストに名前をつける(Dataverseから取得した値を入れるのに必要)
・LIFF固有の記述数行
コピー(HTML)に下記を追加し、保存します。
HTMLコード
<div class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">
<div class="container" style="padding: 0px; display: flex; flex-wrap: wrap;">
<div id="qrcode-url"></div>
</div>
</div>
<div class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">
<div class="container" style="padding: 0px; display: flex; flex-wrap: wrap; column-gap: 0px;">
<div class="col-md-12 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 300px; margin: 20px 0px; padding: 16px; width: calc(100% - 0px);">
<p style="color: var(--portalThemeColor2); text-align: center;">point</p>
<p style="color: var(--portalThemeColor2); text-align: center;" id="point-num">0</p>
<p style="color: var(--portalThemeColor2); text-align: center;">有効期限</p>
<p style="color: var(--portalThemeColor2); text-align: center;" id="expiration-date">2023/1/1</p>
</div>
</div>
</div>
<div class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">
<div class="container" style="padding: 0px; display: flex; flex-wrap: wrap;">
<div class="col-md-12 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 300px;">
<p>●商品購入用バーコード</p>
<p>5秒後に商品購入シミュレーションのため、バーコード読み込みが自動的に実施されます。</p>
</div>
</div>
</div>
<!-- LIFF ID ERROR -->
<div id="liffIdErrorMessage" class="hidden">
<p glot-model="liff-id-error"><!--不正なLIFFIDです。--></p>
</div>
<!-- LIFF INIT ERROR -->
<div id="liffInitErrorMessage" class="hidden">
<p glot-model="liff-init-error"><!--liff.init()が失敗しました。--></p>
</div>
<script charset="utf-8" src="https://static.line-scdn.net/liff/edge/versions/2.8.0/sdk.js"></script>
<script src="https://unpkg.com/glottologist"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
ちなみに、元のコードに追加した差分は下記の赤枠の部分になります。
Dataverseのテーブルの作成
ユーザのポイント情報を保存するテーブルを作成します。
下記のように設定し、「保存」をクリックします。
表示列 | |
---|---|
プロパティ | MembersCardUserInfo |
プライマリ列 | userId |
列を追加します。
下記のように2列追加し、「保存」をクリックします。
表示名 | データの種類 |
---|---|
point | 整数 |
expirationDate | 一行テキスト |
テーブルの作成は完了です。
Power Automate(フロー)の作成
フロー名を入力、HTTP要求の受信時を選択し、「作成」をクリックします。
「変数を初期化する」と検索し、「変数を初期化する」をクリックします。
下記のように設定し、「新しいステップ」をクリックします。
設定値 | |
---|---|
名前 | user_info |
種類 | オブジェクト |
値 | 空 |
こまめに保存しましょう。
同じように下記を「変数を初期化する」に設定します。
名前 | 種類 | 値 |
---|---|---|
before_awarded_point | 整数 | 0 |
次に、ブラウザから受け取ったパラメーターのJSON解析を行います。
パラメータを書き換える場合は、「properties」の中に下記の部分を書き換えます。
変数名: {
"type": "string"
},
設定値 | |
---|---|
コンテンツ | @{triggerBody()} |
スキーマ | 下記のJSON |
jsonコード
{
"type": "object",
"properties": {
"mode": {
"type": "string"
},
"idToken": {
"type": "string"
}
}
}
次に受け取ったidTokenでユーザIDを取得します。
HTTPと検索し、HTTPを選択します。
下記のように設定します。
設定値 | |
---|---|
方法 | POST |
URI | https://api.line.me/oauth2/v2.1/verify |
ヘッダー | { "Content-Type": "application/x-www-form-urlencoded" } |
本文 | id_token=@{body('JSON_の解析')?['idToken']}&client_id={LIFFでコピーしたLIFF IDの数字部分} |
次にDataverseからユーザーのデータがあるか検索します。
「行を一覧にする」と検索し、「行を一覧にする」をクリックします。
下記のように設定します。
設定値 | |
---|---|
テーブル名 | MembersCardUserInfo |
列を選択する | cr08b_userid,cr08b_point,cr08b_expirationdate,cr08b_memberscarduserinfoid |
行のフィルター | cr08b_userid eq '@{body('HTTP')?['sub']}' |
処理を分けて作成しまていきます。
「スイッチ」をクリックします。
設定値 | |
---|---|
オン | @{body('JSON_の解析')?['mode']} |
ケース | init |
ケース2 | buy |
+ボタンでケースを増やします。
【init 側】
「条件」を追加し、ユーザのデータがあるかないかで切り分けます。
設定値 | |
---|---|
左の値 | @{length(outputs('行を一覧にする')?['body/value'])} |
中央の値 | 次の値に等しい |
右の値 | 0 |
[はい の側]
下記を設定し、Dataverseに行を追加します。
設定値 | |
---|---|
テーブル名 | MembersCardUserInfo |
userId | @{body('HTTP')?['sub']} |
point | 0 |
次に画面に返す値を設定します。
設定値 | |
---|---|
名前 | user_info |
値 | 下記のJSON |
jsonコード
{
"userId": @{outputs('新しい行を追加する')?['body/cr08b_userid']},
"pointExpirationDate": @{outputs('新しい行を追加する')?['body/cr08b_expirationdate']},
"point": @{outputs('新しい行を追加する')?['body/cr08b_point']},
"id": @{outputs('新しい行を追加する')?['body/cr08b_memberscarduserinfoid']}
}
[いいえ 側]
ユーザのデータを画面に返す値を設定します。
「それぞれに適用する」をクリックします。
下記を設定します。
設定値 | |
---|---|
以前の手順から出力を選択 | @{outputs('行を一覧にする')?['body/value']} |
変数を設定します。
設定値 | |
---|---|
名前 | user_info |
値 | 下記のJSON |
jsonコード
{
"userId": @{items('Apply_to_each')?['cr08b_userid']},
"pointExpirationDate": @{items('Apply_to_each')?['cr08b_expirationdate']},
"point": @{items('Apply_to_each')?['cr08b_point']},
"id": @{items('Apply_to_each')?['cr08b_memberscarduserinfoid']}
}
【buy 側】
変数の設定をします。
設定値 | |
---|---|
名前 | before_awarded_point |
値 | @{items('Apply_to_each_2')?['cr08b_point']} |
自動的に「Apply to each 2」が作られます。
Dataveseに行を更新します。
設定値 | |
---|---|
テーブル | MembersCardUserInfo |
行ID | @{items('Apply_to_each_2')?['cr08b_memberscarduserinfoid']} |
userId | @{items('Apply_to_each_2')?['cr08b_userid']} |
expirationDate | @{addDays(utcNow(),365,'yyyy/MM/dd')} |
point | @{add(variables('before_awarded_point'),5)} |
変数を設定します。
設定値 | |
---|---|
名前 | user_info |
値 | 下記のJSON |
jsonコード
{
"userId": @{outputs('行を更新する')?['body/cr08b_userid']},
"pointExpirationDate": @{outputs('行を更新する')?['body/cr08b_expirationdate']},
"point": @{outputs('行を更新する')?['body/cr08b_point']},
"id": @{outputs('行を更新する')?['body/cr08b_memberscarduserinfoid']}
}
HTTPを追加し、トーク画面へpushを使って、メッセージを送信します。
設定値 | |
---|---|
方法 | POST |
URI | https://api.line.me/v2/bot/message/push |
ヘッダー | { "Content-Type": "application/json", "Authorization": "Bearer {LINE側でメモしたアクセストークン} } |
本文 | 下記JSON |
jsonコード
{
"to": "@{outputs('行を更新する')?['body/cr08b_userid']}",
"messages": [
{
"type": "flex",
"altText": "お買い上げありがとうございます。電子レシートを発行します。",
"contents": {
"type": "bubble",
"header": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "領収書",
"size": "xxl",
"weight": "bold"
},
{
"type": "text",
"text": "@{utcNow()}",
"color": "#767676"
},
{
"type": "text",
"wrap": true,
"text": "※デジタル会員証のハンズオンアプリであるため、実際の課金は行われません",
"color": "#ff6347"
}
]
},
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "box",
"layout": "vertical",
"margin": "lg",
"spacing": "sm",
"contents": [
{
"type": "box",
"layout": "baseline",
"spacing": "sm",
"contents": [
{
"type": "text",
"text": "チョコレート",
"color": "#5B5B5B",
"size": "sm",
"flex": 5
},
{
"type": "text",
"text": "500",
"wrap": true,
"color": "#666666",
"size": "sm",
"flex": 2,
"align": "end"
}
]
},
{
"type": "box",
"layout": "baseline",
"spacing": "sm",
"contents": [
{
"type": "text",
"text": "お会計金額",
"color": "#5B5B5B",
"size": "sm",
"flex": 5
},
{
"type": "text",
"text": "500",
"wrap": true,
"color": "#666666",
"size": "sm",
"flex": 2,
"align": "end"
}
]
},
{
"type": "box",
"layout": "baseline",
"spacing": "sm",
"contents": [
{
"type": "text",
"text": "付与ポイント",
"color": "#5B5B5B",
"size": "sm",
"flex": 5
},
{
"type": "text",
"text": "5",
"wrap": true,
"color": "#666666",
"size": "sm",
"flex": 2,
"align": "end"
}
]
}
],
"paddingBottom": "xxl"
},
{
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "商品のご購入ありがとうございます。",
"wrap": true,
"size": "sm",
"color": "#767676"
}
]
}
],
"paddingTop": "0%"
}
}
}
]
}
buy側は完了です。
最後にスイッチの下に応答を追加し、値を画面に返します。
設定値 | |
---|---|
状態コード | 200 |
ヘッダー | { "Content-Type": "application/json" } |
本文 | @{variables('user_info')} |
Automateの作成は完了です。
アプリの作成のつづき
下記の画面に戻り、カスタムJavascriptに下記を追加し、保存します。
Javascriptコード
下記に書き換えます。
BASE_URL: {Power AutomateでコピーしたHTTP POST のURL}
liffId: {LIFFでコピーしたLIFF ID}
// 環境設定を読み込む
const BASE_URL = "xxxxxx";
const liffId = "xxx-xxx"
// グローバル変数の宣言
let idToken = "";
let lang = "ja";
//多言語対応のメッセージ読み込み
let message = {}
window.onload = function () {
let myLiffId = liffId;
initializeLiffOrDie(myLiffId);
};
/**
* Check if myLiffId is null. If null do not initiate liff.
* @param {string} myLiffId The LIFF ID of the selected element
*/
function initializeLiffOrDie(myLiffId) {
if (!myLiffId) {
document.getElementById("liffIdErrorMessage").classList.remove("hidden");
} else {
initializeLiff(myLiffId);
}
}
/**
* Initialize LIFF
* @param {string} myLiffId The LIFF ID of the selected element
*/
function initializeLiff(myLiffId) {
liff
.init({
liffId: myLiffId,
})
.then(() => {
// start to use LIFF's api
initializeApp();
})
.catch((err) => {
document
.getElementById("liffInitErrorMessage")
.classList.remove("hidden");
});
}
function initializeApp() {
if (!liff.isLoggedIn()) {
liff.login({redirectUri: location.href});
}
idToken = liff.getIDToken();
getUserData(idToken);
setTimeout(() => demoAddPoint(), 5000);
}
/**
* ログインユーザーの会員データを取得する
* @param {String} idToken
*/
function getUserData(idToken) {
const body = {
"mode": "init",
"idToken": idToken,
};
// URLを開く
let request = new XMLHttpRequest();
request.open("POST", BASE_URL, true);
request.responseType = "json";
request.onload = function () {
console.log(request.status)
if (request.readyState === 4 && request.status === 200) {
data = this.response;
displayPoint(data.point);
displayExpirationDate(data.pointExpirationDate);
setQrcodeUrl("https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl="+ data.userId)
} else {
console.warn(message.serverError[lang]);
}
};
request.send(JSON.stringify(body));
}
/**
* 画面にポイントを表示する
* @param {String} point
*/
function displayPoint(point) {
$("#point-num").text(point);
}
/**
* QRコードにURLを埋める
* @param {String} point
*/
function setQrcodeUrl(url) {
var base;
var obj;
base = document.getElementById("qrcode-url");
base.innerHTML = "";
// IFRAME 作成
obj = document.createElement("iframe");
// IFRAME の見栄え属性をセット
obj.setAttribute("frameBorder", "0");
// IFRAME の配置属性をセット
obj.style.position = "relative";
obj.style.width = "200px";
obj.style.height = "200px";
// IFRAME の内容をセット
obj.src = url;
// IFRAME を実装
base.appendChild(obj);
}
/**
* 画面にポイント期限日を表示する
* @param {String} expirationDate
*/
function displayExpirationDate(expirationDate) {
$("#expiration-date").text(expirationDate);
}
function demoAddPoint() {
const body = {
mode: "buy",
idToken: idToken
};
let request = new XMLHttpRequest();
request.open("POST", BASE_URL, true);
request.responseType = "json";
request.onload = function () {
if (request.readyState === 4 && request.status === 200) {
data = this.response;
displayPoint(data.point);
displayExpirationDate(data.pointExpirationDate);
} else if(request.status === 403) {
if(!alert(message.sessionExpired[lang])){
liff.logout();
liff.login({redirectUri: location.href});
}
} else {
alert(message.error[lang]);
}
};
request.send(JSON.stringify(body));
}
※ 本当はPagesとDataverseのよりシームレスな連携の模索をもっとしたかったですが、いまはPower Automateのエンドポイントを実行する形で着地しています
動作確認
お買い物のシーンでデジタル会員証が大活躍❗️
お会計時にスマホのカメラでデジタル会員証を簡単に開くことができ、店員さんにQRコードをよんでもらってポイントが付与されるという紙を一切使用しない環境にも優しい仕組みです✨
このデモ動画ではQRコードをよんでくれる店員さんはいないので5秒たったらQRコードをよんでくれた体にしています笑
まとめ
Webアプリケーションは現在主力のアプリケーション形式の1つかと思います。
従来ですと、限られた専門家だけがWebアプリの開発を行うことができていましたが、Microsoftのテクノロジーはその従来の概念を打ち砕き、新しい展開を切り拓いていくのではないでしょうか?
現時点ではまだ全てのWebアプリを作れるとまでは言えないかもしれませんが、今後に非常に期待がもてる機能だと言えそうですね!これからのアップデートが楽しみです✨
参考
https://learn.microsoft.com/ja-jp/power-apps/maker/portals/create-portal
https://learn.microsoft.com/ja-jp/power-apps/maker/portals/compose-page
https://developers.google.com/chart/infographics/docs/qr_codes