Mark Ku's Blog
首頁 關於我
Amazon pay 串接筆記
Payment
Amazon pay 串接筆記
Mark Ku
Mark Ku
March 07, 2024
1 min

時空背景

這是我第二次接手 Amaozn pay,上次是因為剛加入公司時,幫忙處理Amazon pay 一直瘋狂掉單,這次重新接 Amazon pay 是為了德國的電商網站,藉由此次重新整理了一下界接的技巧。

相關界接文件

  • 界接文件
  • 如何產生公私鑰

界接流程圖

Amazon pay checkout 其實就很像 payapal express 的 checkout

Amazonpay integration flow
Amazonpay integration flow

首先,取得Amazon pay 相關秘鑰

接著,進入後台 > 選擇Sandbox View > Integration central

Find Integration central
Find Integration central

因為我有兩個國家的帳號,對照之後發現這邊有個bug,有個帳號會找不到Self-Developed,因此無法顯示Create keys按鈕(上傳 public key)功能

Integration central - 1
Integration central - 1
如果你沒有Self-developed 下拉選項,此時可以選擇 Woo Commerce
Integration central - 2
Integration central - 2

依據官方文件Doc , 產生 rsa 公鑰與私鑰

ssh-keygen -t rsa -b 2048 -m PKCS8 -f privateKey.pem
ssh-keygen -f privateKey.pem -e -m PKCS8 > publicKey.pub

因為第一個選項功能應該是壞了,只會產生公鑰,而無法下載私鑰,請選擇第二個選項,請自行上傳 Amazon pay 公鑰,

Upload public key
Upload public key

此時在這畫面,你就能找到大部份所需要的金鑰

All key in here
All key in here

但Store Id 和 Client secret 你需要在另外一個頁面去查詢

store id and secret
store id and secret

測試注意事項

  • 因為Amazon pay付款需要導頁來導頁去,需要有https,也要有自己的domain,我這邊是透過Cloudfalre tunnel 當反向代理測試
  • 正式和測試的金鑰一樣其實蠻方便的,要使用 Sandbox 環境 只要初始化時將 isSandbox 設定成true 即可
  • 關於測試帳號部份,進入後台切到Sandbox view 就能找到 Test accunts,這邊就可以增加sandbox 的測試帳號。

前端範例程式 - React (Next js App route)

'use client';
import { PaymentReturnType } from '@/const/cart/payment-image';
import { PaymentOptions } from '@/const/payment/payment-option';
import {
    useBuildPaymentInfoMutation,
    useCaptureMutation,
    useLazyGetConfigQuery,
    useProcessMutation,
} from '@/redux/api/test-payment-apiSlice';
import { IGeneralPaymentInfo, IGeneralPaymentResult, IGernalPaymentParams } from '@/typing/cart';
import { ApiResponse } from '@/typing/common';
import { useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { v4 as uuidv4 } from 'uuid';

export default function TestAmaonPay() {
    const gernalPaymentParams: IGernalPaymentParams = {
        paymentTypeCode: PaymentOptions[PaymentOptions.Paypal],
        orderNo: uuidv4(),
    } as IGernalPaymentParams;

    const [GetConfig, result] = useLazyGetConfigQuery(); // rtk query 取得sdk 初始化的參數

    const [Process] = useProcessMutation(); // rtk query 用來呼叫建立建立授權訂單的api

    const [Capture] = useCaptureMutation(); // rtk query 用來呼叫提取信用及完成訂單的api
    const [BuildPaymentInfoMutation] = useBuildPaymentInfoMutation(); // rtk query 用來取得Amazon Pay 付款資訊

    const amazonCheckoutSessionId = useRef('');
    const amazonHasRedirectedBack = useRef(false);

    const searchParams = useSearchParams();

    const loadAmazonPay = () => {
        const script = document.createElement('script');

        script.src = 'https://static-eu.payments-amazon.com/checkout.js';
        script.async = true;
        script.onload = () => {
            // debugger;
            if (window.amazon && window.amazon.Pay) {
                GetConfig(gernalPaymentParams.paymentTypeCode)
                    .unwrap()
                    .then((data: any) => {
                        if (!data) {
                            return;
                        }
                        window.amazon.Pay.renderButton('#amazonpaybutton', {
                            merchantId: data.merchantId,
                            sandbox: data.isSandbox === 'True', // dev environment
                            ledgerCurrency: data.ledgerCurrency, // Amazon Pay account ledger currency
                            checkoutLanguage: 'en_GB', // render language
                            productType: 'PayAndShip', // checkout type
                            placement: 'Cart', // button placement
                            buttonColor: 'Gold',
                            createCheckoutSessionConfig: {
                                payloadJSON: data.payloadJSON,
                                signature: data.signature,
                                publicKeyId: data.publicKeyId,
                            },
                        });
                    });
            }
        };
        document.body.appendChild(script);
    };

    const getPaymentInfoFromToken = (token: string) => {
        const params: IGernalPaymentParams = { ...gernalPaymentParams, paymentGatewaySessionID: token };

        BuildPaymentInfoMutation(params)
            .unwrap()
            .then((res: ApiResponse<IGeneralPaymentInfo>) => {
                document.getElementById('paymentInfo')!.innerText = JSON.stringify(res.data);
            });
    };

    useEffect(() => {
        // Dynamically load the Amazon Pay script
        loadAmazonPay();
        const session = searchParams?.get('amazonCheckoutSessionId') || '';

        const isComplete = searchParams?.get('isComplete') || '';

        if (session) {
            amazonCheckoutSessionId.current = session;
            amazonHasRedirectedBack.current = true;

            if (!isComplete) {
                getPaymentInfoFromToken(session);
            } else {
                const params: IGernalPaymentParams = {
                    ...gernalPaymentParams,
                    paymentGatewaySessionID: session,
                };

                Capture(params)
                    .unwrap()
                    .then((res: ApiResponse<IGeneralPaymentResult>) => {
                        if (res.isSuccess) {
                            alert('Payment success');
                        }
                    });
            }
        }
    }, []); // Empty dependency array means this effect runs once on mount

    const clickHandler = () => {
        const params: IGernalPaymentParams = {
            ...gernalPaymentParams,
            paymentGatewaySessionID: amazonCheckoutSessionId.current,
        };

        Process(params)
            .unwrap()
            .then((res: ApiResponse<IGeneralPaymentResult>) => {
                // debugger;
                if (res.isSuccess && res.data.paymentReturnType === PaymentReturnType.RedirectUrl) {
                    location.href = res.data.paymentReturnValue;
                }
            });
    };

    return (
        <div>
            <h1>test amazon pay</h1>
            <div id="amazonpaybutton"></div>
            <div id="paymentInfo"></div>

            <button
                className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
                onClick={clickHandler}
            >
                Checkout
            </button>
        </div>
    );
}

後端程式範例 - C#

首先,Nuget安裝 Amazon.Pay.API.SDK

定義後端設定檔 - Appsetting.json

"Payment": {
    "PaymentOptions": [
 {
   "PaymentName": "AmazonPay",
   "ClientId": "Your clientId", // live and sandbox are the same
   "Secret": "Your clientId secret", // live and sandbox are the same
   "IsSandbox": true,
   "StoreID": "",
   "StoreName": "Your storename",
   "MerchantID": "Your MerchantID", // from amazon 
   "PublicKeyID": "Upload to amazon backoffice PublicKeyID", // 將公鑰上傳amazon pay 產上的ID
   "PrivateKey": "Your private key", // 透過電腦自己產的公鑰,base64 加密起來就不用額外檔案
   "EndPoint": "https://api.paypal.com"
  }
 ]
 }

初始化 Amazon SDK 的 WebStoreClient,因為後面很常使用到,所以抽成一個function

 private WebStoreClient InitiateClient()
 {
    var payConfiguration = new ApiConfiguration
    (
        region: Region.Europe,
        environment: GeneralPaymentConfig.IsSandbox ? Amazon.Pay.API.Types.Environment.Sandbox : Amazon.Pay.API.Types.Environment.Live,
        publicKeyId: GeneralPaymentConfig.PublicKeyID,
        privateKey: System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(GeneralPaymentConfig.PrivateKey))
    );

    var client = new WebStoreClient(payConfiguration);

    return client;
 }

從後端取回init Amazon button 所需要的參數

 public async Task<Result<Dictionary<string, string>>> GetConfig(string returnUrl = "")
 {
    var req = HttpContext.Current.Request;
    var result = new Result<Dictionary<string, string>> { IsSuccess = false, Message = "" };
    try
    {
       if (string.IsNullOrWhiteSpace(returnUrl))
       {
          returnUrl = "/en/test-payment/";
       }

       string ChangePaymentReferID = string.Empty;
       var isChangePayment = !string.IsNullOrEmpty(ChangePaymentReferID);  // for change payment

       var client = InitiateClient();
       var request = new CreateCheckoutSessionRequest(
           checkoutReviewReturnUrl: AppSettingsConstVars.FontendUrl + returnUrl + (isChangePayment ? $"/payments?id={ChangePaymentReferID}" : ""),
           storeId: GeneralPaymentConfig.StoreID
       );
       request.PaymentDetails.CanHandlePendingAuthorization = false;
       request.DeliverySpecifications.AddressRestrictions.Type = RestrictionType.Allowed;
       request.DeliverySpecifications.AddressRestrictions.AddCountryRestriction("DE");

       //generate the button signature
       var signature = client.GenerateButtonSignature(request);
       var payload = request.ToJson();

       result.Data.Add("signature", signature);
       result.Data.Add("payloadJSON", payload);
       result.Data.Add("publicKeyId", GeneralPaymentConfig.PublicKeyID);
       result.Data.Add("merchantId", GeneralPaymentConfig.MerchantID);
       result.Data.Add("isSandbox", GeneralPaymentConfig.IsSandbox.ToString()); // need test 
       result.Data.Add("ledgerCurrency", HardCodeKey.BasedCurrency);

       result.Success();
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][GetConfig]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }

接著實作取回付款地址

 public async Task<Result<GeneralPaymentInfo>> GetPaymentInfoAsync(GernalPaymentParameter paymentParameter)
 {
    Result<GeneralPaymentInfo> result = new Result<GeneralPaymentInfo> { IsSuccess = false, Message = "" };

    try
    {
       var client = InitiateClient();
       var getInfo = client.GetCheckoutSession(paymentParameter.PaymentGatewaySessionID);

       if (getInfo == null)
       {
          throw new Exception($"[AmazonPayService][GetPaymentInfoAsync]:{getInfo.RawResponse}");
       }

       result.Data.Address = ConvertToGeneralAddress(getInfo.ShippingAddress, getInfo.Buyer.Email);

       result.Success();
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][GetPaymentInfoAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }
 
 private GeneralAddress ConvertToGeneralAddress(Address amazonShipAddress, string Mail)
{
   GeneralAddress generalAddress = new GeneralAddress();
   generalAddress.Line1 = amazonShipAddress.AddressLine1;
   generalAddress.Line2 = (string.IsNullOrEmpty(amazonShipAddress.AddressLine2) && string.IsNullOrEmpty(amazonShipAddress.AddressLine3)) ? "" : string.Join(",", amazonShipAddress.AddressLine2, amazonShipAddress.AddressLine3);
   generalAddress.FirstName = GetMaxLengthStr(ParseName(amazonShipAddress.Name).Item1, 80);
   generalAddress.LastName = GetMaxLengthStr(ParseName(amazonShipAddress.Name).Item2, 80);
   generalAddress.CountryCode = amazonShipAddress.CountryCode;
   generalAddress.PostalCode = amazonShipAddress.PostalCode;
   generalAddress.City = amazonShipAddress.City;
   generalAddress.StateProvinceCode = amazonShipAddress.StateOrRegion;
   generalAddress.Phone = amazonShipAddress.PhoneNumber;
   return generalAddress;
}

private string GetMaxLengthStr(string Text, int Max)
{
   if (string.IsNullOrWhiteSpace(Text))
   {
      return "";
   }

   return Text.Substring(0, Math.Min(Max, Text.Length));
}

private Tuple<string, string> ParseName(string Name)
{
   if (string.IsNullOrWhiteSpace(Name))
   {
      return Tuple.Create("", "");
   }

   var names = Name.Split(' ');

   if (names.Length == 1)
   {
      return Tuple.Create(Name, "");
   }

   var last = names.Last();

   return Tuple.Create(Name.Replace(last, "").Trim(), last);
} 

取得 Amazon 交易頁面 Url( PurchaseGetAmazonPayRedirectUrl )

 public async Task<Result<GeneralPaymentResult>> ProcessAsync(GernalPaymentParameter paymentParameter, string returnUrl)
 {
    Result<GeneralPaymentResult> result = new Result<GeneralPaymentResult> { IsSuccess = false, Message = "Please select another credit card or change payment method and try again." };

    try
    {
       var client = InitiateClient();
       var request = new UpdateCheckoutSessionRequest();

       if (string.IsNullOrWhiteSpace(returnUrl))
       {
          returnUrl = "/en/test-payment/?isComplete=true";
       }

       request.WebCheckoutDetails.CheckoutResultReturnUrl = AppSettingsConstVars.FontendUrl + returnUrl;

       var currencyCode = (Currency)Enum.Parse(typeof(Currency), HardCodeKey.BasedCurrency);

       request.PaymentDetails.ChargeAmount.Amount = HardCodeKey.Payment.TestAmount;
       request.PaymentDetails.ChargeAmount.CurrencyCode = currencyCode;
       request.PaymentDetails.CanHandlePendingAuthorization = false;
       request.PaymentDetails.PaymentIntent = Authorize ? PaymentIntent.Authorize : PaymentIntent.AuthorizeWithCapture;

       NLogUtil.WriteSEQLog($"[AmazonPay][AmazonV2Helper][PurchaseGetAmazonPayRedirectUrl][UpdateCheckoutSession][Authorize]:{Authorize}", NLog.LogLevel.Info);
       request.MerchantMetadata.MerchantReferenceId = paymentParameter.OrderNo;

       request.MerchantMetadata.MerchantStoreName = GeneralPaymentConfig.StoreName;
       request.MerchantMetadata.NoteToBuyer = GetProductDescription();

       var updateResult = client.UpdateCheckoutSession(paymentParameter.PaymentGatewaySessionID, request);

       if (updateResult.Success)
       {
          result.Data.PaymentReturnType = PaymentReturnType.RedirectUrl;
          result.Data.PaymentReturnValue = updateResult.WebCheckoutDetails.AmazonPayRedirectUrl;
          result.Success();

          // TODO Remove this log when stable
          NLogUtil.WriteSEQLog($"[AmazonPayService][ProcessAsync][UpdateCheckoutSession][Success]{JsonConvert.SerializeObject(updateResult)}", NLog.LogLevel.Info);
       }
       else
       {
          NLogUtil.WriteSEQLog($"[AmazonPayService][ProcessAsync][UpdateCheckoutSession][Error]Request:{JsonConvert.SerializeObject(request)},Response:{JsonConvert.SerializeObject(updateResult)}", NLog.LogLevel.Error);
       }
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][ProcessAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }

完成訂單及請款(Capture)

 public virtual async Task<Result<GeneralPaymentResult>> CaptureAsync(GernalPaymentParameter paymentParameter)
 {
    var result = new Result<GeneralPaymentResult> { IsSuccess = true, Message = "No need implemented" };

    try
    {
       var currencyCode = (Currency)Enum.Parse(typeof(Currency), HardCodeKey.BasedCurrency);
       var client = InitiateClient();
       var request = new CompleteCheckoutSessionRequest(HardCodeKey.Payment.TestAmount, currencyCode);
       CheckoutSessionResponse complete = client.CompleteCheckoutSession(paymentParameter.PaymentGatewaySessionID, request);

       if (complete.Success)
       {
          var paymentTransition = new GeneralPaymentTransition();
          paymentTransition.Authcode = complete.ChargeId;
          paymentTransition.TransactionID = complete.ChargeId;
          paymentTransition.Amount = HardCodeKey.Payment.TestAmount.ToString(); // todo
          paymentParameter.PaymentTypeCode = GeneralPaymentConfig.PaymentName;
          result.Data.PaymentTransition = paymentTransition;
          NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync][Complete][Success]complete:{JsonConvert.SerializeObject(complete)}", NLog.LogLevel.Info);
          result.Success();
       }
       else
       {
          NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync][Complete][Fail]complete:{JsonConvert.SerializeObject(complete)}", NLog.LogLevel.Error);
       }            
    }
    catch (Exception ex)
    {
       NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
    }

    return result;
 }

最後

Amazon pay 界接方式真的蠻復古的,導頁來導頁去,而且後台常常會有功能改壞,接了兩次,其實不是很好界接。


Tags

Mark Ku

Mark Ku

Software Developer

9年以上豐富網站開發經驗,開發過各種網站,電子商務、平台網站、直播系統、POS系統、SEO 優化、金流串接、AI 串接,Infra 出身,帶過幾次團隊,也加入過大團隊一起開發。

Expertise

前端(React)
後端(C#)
網路管理
DevOps
溝通
領導

Social Media

facebook github website

Related Posts

串接 Paypal 筆記
串接 Paypal 筆記
March 01, 2024
1 min

Quick Links

關於我

Social Media