【Amplify×Flutter】Lambdaを経由してPush通知を送信する方法

前回解説した「【Flutter×Amplify】Push通知機能実装 Android編」では、ユーザーのデバイストークンを取得し、AWS Pinpointのコンソールからデバイスに対してPush通知をテスト送信する手順を解説した。

しかし、アプリ開発の過程で欲しい機能というのは、ユーザーの操作に応じて動的にPush通知を送信する機能である。結論を書くと、今回はAmplify×Flutter単体で完結するものではなく、AWS LambdaやAPI Gatewayを使用して、Pinpointを操作するためのバックエンド処理を実装する必要がある。今回は、その方法について解説する。

構成は以下

この記事のゴールと前提

Android端末でログインしたユーザーのアプリがバックグラウンド状態になった際、Puhs通知を受信できること。※アプリがバックグラウンドになった場合の判定処理などはこの記事では解説しない。

前提

  • Amplify×Flutter環境でPush通知を実現するための初期設定、テスト送信が完了していること。
  • アプリでCognitoを使った認証機能の実装が完了しており、ログインユーザーのAWS認証情報などが取得できていること。

実装

必要な処理

Android端末でPush通知を送信するためには、以下の処理が必要になる。
Flutter側とLambda側でそれぞれ実装する必要がある。

  1. 【Flutter】AWS Pinpointにユーザーの情報を登録・更新する
  2. Lambda関数を作成する
    • AWS Pinpointのエンドポイントを取得する処理
    • Pinpointのエンドポイントを登録または更新する処理
    • Push通知の送信処理
  3. IAMロール、ポリシーの設定(Lambda関数の作成と並行して行う)
  4. LambdaとAPI Gatewayの連携
  5. 【Flutter】AWS認証情報を取得してHTTPリクエストを行う

1.【Flutter】AWS Pinpointにユーザーの情報を登録・更新する

Flutterで提供されているAmplify.Analytics.identifyUserというメソッドは、Amazon Pinpointでユーザーのプロファイル情報を設定するためのもの。これにより、Pinpointの分析やセグメント作成においてユーザーの属性情報を利用することができる。

アプリ内で以下のコードを実行する。私の場合、ユーザーがログインに成功したタイミングで実施している。この処理を実行することで、AWS Pinpointにユーザーの情報を登録、更新できる。

/*
  Pinpointにユーザーの情報を登録または更新する
  このコードを実行することで、アプリケーションのユーザー情報をAmazon Pinpointに識別および提供する。
*/
Future<void> addAnalyticsWithLocation(String userId, String email) async {
  final userProfile = UserProfile(
    email: email,
  );
  await Amplify.Analytics.identifyUser(
    userId: userId,
    userProfile: userProfile,
  );
}

公式ドキュメント: Identify user to Amazon Pinpoint

Lambda関数の作成

Lambdaについては以下を参照。

AWS Lambda の特徴

なぜLambda関数でPush通知の処理を作成するのか

  • AWSサービスとの連携が簡単で高速
    • 今回はユーザーデバイスへのPush通知の送信にAWS Pinpointを使用している。LambdaはサーバレスにAWSのサービスを連携することが簡単なので、開発者はSDKを使用してPinpointとの連携処理を書くだけでよく、サーバーの保守管理が必要なくなる。
  • セキュリティ
    • 通知サービスのAPIキーやトークンなど、秘密の情報をクライアントサイドに直接書き込むことはセキュリティ上のリスクとなる。バックエンドを経由することで、これらの情報を安全に隠蔽することができる。
  • ロジックの分離
    • 上記のセキュリティにも関連するが、Push通知の送信処理などPinpointに関連するロジックをバックエンドに定義することで、フロントエンドの責務をUIとAPIの呼び出しのみに分離できる。

関数の作成

AWS コンソール > Lambda >「関数を作成」から関数を作成する。

作成画面で以下の設定を行う。

  • 「一から作成」を選択
  • 関数名: わかりやすい任意の名称をつける
  • ランタイム: 「Node.js 18.x」を選択
    • 私はJavaScriptのエンジニアなのでNode.jsを選択したが、PythonやJavaなどのプログラミング言語も選択できる。
  • アーキテクチャ: x86_64

関数を作成したら、以下のようにコードが編集できるようになるので、ここに以下を実現するための処理を記述する。

  • AWS Pinpointのエンドポイントを取得する処理
  • Pinpointのエンドポイントを登録または更新する処理
  • Push通知の送信処理

AWS Pinpointを操作するための処理を実装する

コードの全体像は以下のようになる。処理の詳細についてはコメントを参照。
AWS SDK for JavaScriptを使用して実装した。

import { PinpointClient, GetUserEndpointsCommand, UpdateEndpointCommand, SendMessagesCommand } from '@aws-sdk/client-pinpoint';

const applicationId = process.env.AWS_PINPOINT_PROJECT_ID;

/**
 * デバイスの種類に応じたメッセージ生成。以下の区分となる。
 * @param channelType チャンネルタイプ(APNS | GCM)
 * 1. iPhone: APNS
 * 2. Android: GCM
 */
const buildMessageConfiguration = (channelType, title, body) => {
  const messageConfig = {
    Action: 'OPEN_APP',
    Title: title,
    Body: body,
  };

  switch (channelType) {
    case 'APNS':
      return { APNSMessage: messageConfig };
    case 'GCM':
      return { GCMMessage: messageConfig };
    // 他のChannelTypeに対するメッセージ設定もこちらに追加
    default:
      return null;
  }
};

/**
 * AWS Pinpointのエンドポイントを取得する
 */
const getEndpoint = async (client, userId) => {
  const params = {
    ApplicationId: applicationId,
    UserId: userId
  };

  return await client.send(new GetUserEndpointsCommand(params));
};

/**
 * AWS Pinpointのエンドポイントを更新する
 */
const updateEndpoint = async (client, EndpointId, EndpointRequest) => {
  const updateParams = {
    ApplicationId: applicationId,
    EndpointId: EndpointId,
    EndpointRequest: EndpointRequest
  };

  return await client.send(new UpdateEndpointCommand(updateParams));
};

/**
 * デバイスにPush通知を送信する
 */
const sendPushNotification = async (client, channelType, title, body, address) => {
  const messageConfig = buildMessageConfiguration(channelType, title, body);
  if (!messageConfig) {
    throw new Error(`Unsupported ChannelType: ${channelType}`);
  }

  const pushInput = {
    ApplicationId: applicationId,
    MessageRequest: {
      Addresses: {
        [address]: {
          ChannelType: channelType,
        },
      },
      MessageConfiguration: messageConfig
    },
  };

  return await client.send(new SendMessagesCommand(pushInput));
};

const errorResponse = (err) => {
  console.log(err);
  return {
    statusCode: 500,
    body: JSON.stringify(err)
  };
};


export const handler = async (event) => {
  const requestBody = event.body;
  const client = new PinpointClient({ region: requestBody.region });

  try {
    switch (requestBody.action) {
      case 'getEndpoint':
        const result = await getEndpoint(client, requestBody.userId);
        return { statusCode: 200, body: JSON.stringify(result) };

      case 'updateEndpoint':
        const updateResult = await updateEndpoint(client, requestBody.EndpointId, requestBody.EndpointRequest);
        return { statusCode: 200, body: JSON.stringify(updateResult) };

      case 'sendPushNotification':
        const pushResponse = await sendPushNotification(client, requestBody.channelType, requestBody.title, requestBody.body, requestBody.address);
        return { statusCode: 200, body: JSON.stringify(pushResponse) };

      default:
        return { statusCode: 400, body: JSON.stringify({ message: 'Invalid action' }) };
    }
  } catch (err) {
    return errorResponse(err);
  }
};

AWS SDK for JavaScriptを使用して実装するにあたり参照したリソースを以下に記載する。

参考ドキュメント

IAMロール、ポリシーの設定

Lambda関数を作成しても、実行ユーザーがPinpointを操作できる権限を持っていなければ処理を実行することはできない。

新しいIAMポリシーを作成し、以下を定義する。私は「Reboot-Dev-LambdaPinpointAccessPolicy」という名称のポリシーにした。以下の記述をすることで、Lambdaに対してPinpointの操作を許可している。現状、以下のアクションのみを許可しているが、拡張する場合には追記する。

IAM ポリシーの Amazon Pinpointアクション

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PinpointPolicy",
            "Effect": "Allow",
            "Action": [
                "mobiletargeting:GetUserEndpoints",
                "mobiletargeting:UpdateEndpoint",
                "mobiletargeting:GetEndpoint",
                "mobiletargeting:SendMessages",
                "mobiletargeting:SendUsersMessages"
            ],
            "Resource": "arn:aws:mobiletargeting:*:136112039144:apps/*"
        }
    ]
}

IAMポリシーを作成したら、以下の手順でポリシーをロールにアタッチする。

  • IAM > ロールで、Lambda関数を作成した際に生成されたロールを選択する
  • 許可セクションでロールをアタッチ(下記画像を参照)
    • 作成したポリシーを選択できるので、アタッチする

これで、Lambdaを実行するユーザーがPinpointを操作できるようになる。

LambdaとAPI Gatewayの連携を行う

Lambda関数を作成したら、Pinpointを操作するための準備は整った。しかし、私が実装している環境では、クライアント側から直接Lambda関数を直接呼び出す構成にはっていない。API Gatewayを経由してLambdaの処理を呼び出すようにしている。

API呼び出しのリクエストにユーザーの認証情報を付与し、認証に成功しているユーザーのみが処理を実行できるようにしたいため。

LambdaとAPI Gatewayの連携については、以下の記事を参照して実施する。

【AWS】API GatewayとLambdaを連携させる手順

5.【Flutter】AWS認証情報を取得してHTTPリクエストを行う

Flutter×Amplify環境にてユーザーの認証情報を取得し、HTTPリクエストを送信するためには大きく分けて以下の工程になる。

  • AWS認証情報(CognitoUserPoolToken)を取得する処理
  • AWSにHTTPリクエストを送信する処理

AWS認証情報(CognitoUserPoolToken)を取得する処理

以下の公式ドキュメントにも記載があるが、以下の処理を実行することでidTokenなどの認証情報を取得することができる。

私の実装環境では、APIを呼び出す際のリクエストヘッダーにidTokenを付与するため、以下の処理で取得したデータから、idTokenを取り出してデータを保持している(idTokenを保持する処理は別で作っている)。

参照: Accessing credentials

Future<void> fetchCognitoAuthSession() async {
  try {
    final cognitoPlugin = Amplify.Auth.getPlugin(AmplifyAuthCognito.pluginKey);
    final result = await cognitoPlugin.fetchAuthSession();
    final identityId = result.identityIdResult.value;
    safePrint("Current user's identity ID: $identityId");
  } on AuthException catch (e) {
    safePrint('Error retrieving auth session: ${e.message}');
  }
}

AWSにHTTPリクエストを送信する処理

flutterでHTTPリクエストを送信するために以下のライブラリを使用する。

https://pub.dev/packages/http

pubspec.yamlで定義し、flutter pub getを実行する。

dependencies:
  http: ^1.1.0

HTTPリクエスト処理は以下のように実装。私の場合、別関数でリクエストボディを作成し、以下の共通関数を呼び出している。

// AWS API エンドポインt
final host = 'xxx.execute-api.ap-northeast-1.amazonaws.com';
final path = 'xxxx';

/*
  AWSにHTTPリクエストを送信する共通処理
  @param body リクエストbody
*/
Future<Map<String, dynamic>> callAWSHttpMethod(
  Map<String, dynamic> body,
) async {
  // idTokenを取得
  final idToken = await fetchCognitoUserPoolTokens();
  // httpリクエストを送信
  http.Response response = await http.post(
    Uri.https(
      host,
      path,
    ),
    headers: {'Authorization': idToken},
    body: utf8.encode(json.encode(body)),
  );
  safePrint('Response body: ${response.body}');
  return json.decode(response.body);
}

関連記事

コメント

この記事へのコメントはありません。