怠慢プログラマーの備忘録

怠慢でナマケモノなプログラマーの備忘録です。

【Golang x Stripe】STPEphemeralKeyのDecode問題にハマった話(備忘録)->解決済

【2020/07/28 更新】RowJSONについて

key.RowJSONの型が[]byteとなっています。

どうやら[]byteJSONにされた時点Base64エンコードされた文字列になっているようなので返却値の型を[]byteにしているとBase64の文字列が返却されていました。

つまり

func EphemeralKey(request EphemeralKeysRequest) ([]byte, error) { 
   ...
   return key.RowJSON, nil
}

だとBase64の文字列がそのまま返却されてしまうので、

func EphemeralKey(request EphemeralKeysRequest) (json.RawMessage, error) {
   ...
   return key.RowJSON, nil
}

に変更するとレスポンスが以下の通りになります。

{
    "id": "ephkey_xxxxxxxxxxxxxxxxxx",
    "object": "ephemeral_key",
    "associated_objects": [
        {
            "type": "customer",
            "id": "cus_xxxxxxxxxxxxxxxxx"
        }
    ],
    "created": 100000000,
    "expires": 100000000,
    "livemode": false,
    "secret": "ek_test_xxxxxxxxxxxxxxx"
}

SDK側にStripeIDがnullableではない状態だったので、できないような気もしたのですが、クライアント側でSTPPaymentContext()に噛ませてみたら無事プリセットされたUIが表示されました。 (多分だけどクライアント側SDK内のdecodedObjectFromAPIResponseがうまくこの辺とってきてくれてたりするのかな...🤔)

Twitterでこの記事を投げていたら社内のサーバーサイドエンジニアさん(@shogo82148)が救済してくれました🎉🙇‍♂️

github.com

StripeSDKにプリセットされているカード情報入力UIの表示(STPCustomerContext)にはEphemeralKey(ワンタイムトークン)が必要です。

let customerContext = STPCustomerContext(keyProvider: MyAPIClient())
self.paymentContext = STPPaymentContext(customerContext: customerContext)
self.paymentContext.delegate = self
self.paymentContext.hostViewController = self
self.paymentContext.paymentAmount = 5000 // This is in cents, i.e. $50 USD

stripe.com

基本的に上記の公式ドキュメント通り進めていたのですが...

まずサーバーサイド側。こちらはGolangのAWSLambdaにある関数をAPIGatewayで発行しています。

package main

import (
    "fmt"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/stripe/stripe-go"
    "github.com/stripe/stripe-go/ephemeralkey"
    "log"
)

type EphemeralKeysRequest struct {
    CustomerId string `json:"customer_id"`
    ApiVersion string `json:"api_version"`
}

func EphemeralKey(request EphemeralKeysRequest) (*stripe.EphemeralKey, error) {
    stripe.Key = "sk_test_xxxxxxxxxxxx"

    params := &stripe.EphemeralKeyParams{
        Customer: stripe.String(request.CustomerId),
        StripeVersion: stripe.String(request.ApiVersion),
    }
    key, error := ephemeralkey.New(params)
    if error != nil {
        log.Fatal(error)
    }

    return  key, nil
}

func main() {
    lambda.Start(EphemeralKey)
}

これのレスポンスが以下の通りです。

{
    "associated_objects": [
        {
            "id": "cus_xxxxxxxxxxx",
            "type": "customer"
        }
    ],
    "created": 1000000000,
    "expires": 1000000000,
    "id": "ephkey_xxxxxxxxxxxxxxxx",
    "livemode": false
}

これを公式ドキュメントにもあるとおりクライアント側(今回はiOS側)でSerializeすると、、、

Could not parse the ephemeral key response following protocol STPCustomerEphemeralKeyProvider. Make sure your backend is sending the unmodified JSON of the ephemeral key to your app. For more info, see https://stripe.com/docs/mobile/ios/standard#prepare-your-api

とエラーとなります。

これの箇所が、SDK内のSTPEphemeralKeyManager.m内の(void)_createKeyの以下の箇所です。

STPJSONResponseCompletionBlock jsonCompletion = ^(NSDictionary *jsonResponse, NSError *error) {
        STPEphemeralKey *key = [STPEphemeralKey decodedObjectFromAPIResponse:jsonResponse];
        if (key) {
            [self.createKeyPromise succeed:key];
        } else {
            // the API request failed
            if (error) {
                [self.createKeyPromise fail:error];
            } else {
                // the ephemeral key could not be decoded
                [self.createKeyPromise fail:[NSError stp_ephemeralKeyDecodingError]];
                if ([self.keyProvider conformsToProtocol:@protocol(STPCustomerEphemeralKeyProvider)]) {
                    NSAssert(NO, @"Could not parse the ephemeral key response following protocol STPCustomerEphemeralKeyProvider. Make sure your backend is sending the unmodified JSON of the ephemeral key to your app. For more info, see https://stripe.com/docs/mobile/ios/standard#prepare-your-api");
                } else if ([self.keyProvider conformsToProtocol:@protocol(STPIssuingCardEphemeralKeyProvider)]) {
                    NSAssert(NO, @"Could not parse the ephemeral key response following protocol STPIssuingCardEphemeralKeyProvider. Make sure your backend is sending the unmodified JSON of the ephemeral key to your app. For more info, see https://stripe.com/docs/mobile/ios/standard#prepare-your-api");
                }
                NSAssert(NO, @"Could not parse the ephemeral key response. Make sure your backend is sending the unmodified JSON of the ephemeral key to your app. For more info, see https://stripe.com/docs/mobile/ios/standard#prepare-your-api");
            }
        }

STPEphemeralKeyは手前でインスタンス化することは不可能で必ずdecodedObjectFromAPIResponseを経由する必要があります。

最大の落とし穴

サーバーサイド側のstripe.EphemeralKeyクラスにRawJSONというプロパティがあります。 これが、

RawJSON is provided so that it may be passed back to the frontend unchanged.  Ephemeral keys are issued on behalf of another client which may be running a different version of the bindings and thus expect a different JSON structure.  This ensures that if the structure differs from the version of these bindings, we can still pass back a compatible key.

なんて記述があります。しかしこれを返却したとしてもクライアント側で[String: Any]にキャストできません。

解決(正しいかはわからない)

STPEphemeralKeyの中(SDK内)を覗くと、

@property (nonatomic, readonly) NSString *stripeID;
@property (nonatomic, readonly) NSDate *created;
@property (nonatomic, readonly) BOOL livemode;
@property (nonatomic, readonly) NSString *secret;
@property (nonatomic, readonly) NSDate *expires;
@property (nonatomic, readonly, nullable) NSString *customerID;
@property (nonatomic, readonly, nullable) NSString *issuingCardID;

とあります。

サーバーサイド側で返却しているプロパティにstripeIDsecretに該当するものがないのが原因でした。

なのでひとまずLambda側のStructを返却するように変更しました。

package main

import (
    "fmt"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/stripe/stripe-go"
    "github.com/stripe/stripe-go/ephemeralkey"
    "log"
)

type EphemeralKeysRequest struct {
    CustomerId string `json:"customer_id"`
    ApiVersion string `json:"api_version"`
}

type Response struct {
    //EphemeralKeys []byte `json:"ephemeralKeys"`
    AssociatedObjects []struct {
        ID   string `json:"id"`
        Type string `json:"type"`
    } `json:"associated_objects"`

    Created  int64  `json:"created"`
    Expires  int64  `json:"expires"`
    ID       string `json:"id"`
    Livemode bool   `json:"livemode"`
    StripeID string `json:"stripeID"`
    Secret   string `json:"secret"`
}

func EphemeralKey(request EphemeralKeysRequest) (Response, error) {
    stripe.Key = "sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

    params := &stripe.EphemeralKeyParams{
        Customer: stripe.String(request.CustomerId),
        StripeVersion: stripe.String(request.ApiVersion),
    }
    key, error := ephemeralkey.New(params)
    if error != nil {
        log.Fatal(error)
    }

    return  Response {
        key.AssociatedObjects,
        key.Created,
        key.Expires,
        key.ID,
        key.Livemode,
        "pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxx",
        stripe.Key,
    }, nil
}

func main() {
    lambda.Start(EphemeralKey)
}

Sandbox環境だからかなんなのか、issueにでも書いてみます。

スターティングGo言語

スターティングGo言語