Commit be2ad6c6 authored by Rouslan Solomakhin's avatar Rouslan Solomakhin Committed by Commit Bot

[Payments] Supported origins.

If a payment method manifest contains "supported_origins":
["https://alicepay.com"], then all apps from "https://alicepay.com" with
valid signatures can claim support. These apps must match the package
name, minimum version, and SHA256 certificate fingerprint of the web app
manifest from "https://alicepay.com", which is linked as
"default_applications" from a payment method that has
"https://alicepay.com" origin.

Bug: 735184
Change-Id: I1abdca115434769e75570dd8569facf497b6ef65
Reviewed-on: https://chromium-review.googlesource.com/585171Reviewed-by: default avatarGanggui Tang <gogerald@chromium.org>
Commit-Queue: Rouslan Solomakhin <rouslan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#491059}
parent 26c8b12c
...@@ -16,10 +16,12 @@ public class PaymentManifestWebDataService { ...@@ -16,10 +16,12 @@ public class PaymentManifestWebDataService {
/** /**
* Called when getPaymentMethodManifest success. * Called when getPaymentMethodManifest success.
* *
* @param appPackageNames The supported apps' package names in the payment method manifest. * @param appIdentifiers The list of package names and origins of the supported apps in the
* payment method manifest. May also contain "*" to indicate that
* all origins are supported.
*/ */
@CalledByNative("PaymentManifestWebDataServiceCallback") @CalledByNative("PaymentManifestWebDataServiceCallback")
void onPaymentMethodManifestFetched(String[] appPackageNames); void onPaymentMethodManifestFetched(String[] appIdentifiers);
/** /**
* Called when getPaymentWebAppManifest success. * Called when getPaymentWebAppManifest success.
...@@ -74,10 +76,11 @@ public class PaymentManifestWebDataService { ...@@ -74,10 +76,11 @@ public class PaymentManifestWebDataService {
* Adds the supported Android apps' package names of the method. * Adds the supported Android apps' package names of the method.
* *
* @param methodName The method name. * @param methodName The method name.
* @param appPackageNames The supported apps' package names. * @param appPackageNames The supported app package names and origins. Also possibly "*" if
* applicable.
*/ */
public void addPaymentMethodManifest(String methodName, String[] appPackageNames) { public void addPaymentMethodManifest(String methodName, String[] appIdentifiers) {
nativeAddPaymentMethodManifest(mManifestWebDataServiceAndroid, methodName, appPackageNames); nativeAddPaymentMethodManifest(mManifestWebDataServiceAndroid, methodName, appIdentifiers);
} }
/** /**
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.payments;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.UrlConstants;
import java.net.URI;
import java.net.URISyntaxException;
import javax.annotation.Nullable;
/** URI utilities. */
public class UriUtils {
/** The hostname used by the embedded test server in testing. */
public static final String LOCALHOST_FOR_TEST = "127.0.0.1";
/** The beginning part of the URL used by the embedded test server in testing. */
private static final String LOCALHOST_URI_PREFIX_FOR_TEST = "http://127.0.0.1:";
/** Whether HTTP localhost URIs should be allowed. Should be used only in testing. */
private static boolean sAllowHttpForTest;
/**
* Checks whether the given <code>method</code> string has the correct format to be a URI
* payment method name. Does not perform complete URI validation.
*
* @param method The payment method name to check.
* @return Whether the method name has the correct format to be a URI payment method name.
*/
public static boolean looksLikeUriMethod(String method) {
return method.startsWith(UrlConstants.HTTPS_URL_PREFIX)
|| (sAllowHttpForTest && method.startsWith(LOCALHOST_URI_PREFIX_FOR_TEST));
}
/**
* Parses the input <code>method</code> into a URI payment method name. Returns null for
* invalid URI format or a relative URI.
*
* @param method The payment method name to parse.
* @return The parsed URI payment method name or null if not valid.
*/
@Nullable
public static URI parseUriFromString(String method) {
URI uri;
try {
// Don't use java.net.URL, because it performs a synchronous DNS lookup in the
// constructor.
uri = new URI(method);
} catch (URISyntaxException e) {
return null;
}
if (!uri.isAbsolute()) return null;
assert UrlConstants.HTTPS_SCHEME.equals(uri.getScheme())
|| (sAllowHttpForTest && UrlConstants.HTTP_SCHEME.equals(uri.getScheme())
&& LOCALHOST_FOR_TEST.equals(uri.getHost()));
return uri;
}
/**
* Returns the origin part of the given URI.
*
* @param uri The input URI for which the origin needs to be returned. Should not be null.
* @return The origin of the input URI. Never null.
*/
public static URI getOrigin(URI uri) {
assert uri != null;
// Tests use sub-directories to simulate different hosts, because the test web server runs
// on a single localhost origin. Therefore, the "origin" in test is
// https://127.0.0.1:12355/components/test/data/payments/bobpay.xyz instead of
// https://127.0.0.1:12355.
String originString = uri.resolve(sAllowHttpForTest ? "." : "/").toString();
// Strip the trailing slash.
if (!originString.isEmpty() && originString.charAt(originString.length() - 1) == '/') {
originString = originString.substring(0, originString.length() - 1);
}
URI origin = parseUriFromString(originString);
assert origin != null;
return origin;
}
@VisibleForTesting
public static void allowHttpForTest() {
sAllowHttpForTest = true;
}
private UriUtils() {}
}
\ No newline at end of file
...@@ -784,6 +784,7 @@ chrome_java_sources = [ ...@@ -784,6 +784,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/payments/ServiceWorkerPaymentAppBridge.java", "java/src/org/chromium/chrome/browser/payments/ServiceWorkerPaymentAppBridge.java",
"java/src/org/chromium/chrome/browser/payments/ShippingStrings.java", "java/src/org/chromium/chrome/browser/payments/ShippingStrings.java",
"java/src/org/chromium/chrome/browser/payments/SslValidityChecker.java", "java/src/org/chromium/chrome/browser/payments/SslValidityChecker.java",
"java/src/org/chromium/chrome/browser/payments/UriUtils.java",
"java/src/org/chromium/chrome/browser/payments/ui/BillingAddressAdapter.java", "java/src/org/chromium/chrome/browser/payments/ui/BillingAddressAdapter.java",
"java/src/org/chromium/chrome/browser/payments/ui/Completable.java", "java/src/org/chromium/chrome/browser/payments/ui/Completable.java",
"java/src/org/chromium/chrome/browser/payments/ui/ContactDetailsSection.java", "java/src/org/chromium/chrome/browser/payments/ui/ContactDetailsSection.java",
......
...@@ -147,7 +147,7 @@ public class AndroidPaymentAppFinderTest { ...@@ -147,7 +147,7 @@ public class AndroidPaymentAppFinderTest {
Bundle activityMetaData = new Bundle(); Bundle activityMetaData = new Bundle();
activityMetaData.putString( activityMetaData.putString(
AndroidPaymentAppFinder.META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME, AndroidPaymentAppFinder.META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME,
"https://alicepay.com"); "basic-card");
alicePay.activityInfo.metaData = activityMetaData; alicePay.activityInfo.metaData = activityMetaData;
activities.add(alicePay); activities.add(alicePay);
...@@ -184,7 +184,7 @@ public class AndroidPaymentAppFinderTest { ...@@ -184,7 +184,7 @@ public class AndroidPaymentAppFinderTest {
Bundle alicePayMetaData = new Bundle(); Bundle alicePayMetaData = new Bundle();
alicePayMetaData.putString( alicePayMetaData.putString(
AndroidPaymentAppFinder.META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME, AndroidPaymentAppFinder.META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME,
"https://alicepay.com"); "basic-card");
alicePayMetaData.putInt(AndroidPaymentAppFinder.META_DATA_NAME_OF_PAYMENT_METHOD_NAMES, 1); alicePayMetaData.putInt(AndroidPaymentAppFinder.META_DATA_NAME_OF_PAYMENT_METHOD_NAMES, 1);
alicePay.activityInfo.metaData = alicePayMetaData; alicePay.activityInfo.metaData = alicePayMetaData;
activities.add(alicePay); activities.add(alicePay);
...@@ -197,7 +197,7 @@ public class AndroidPaymentAppFinderTest { ...@@ -197,7 +197,7 @@ public class AndroidPaymentAppFinderTest {
Bundle bobPayMetaData = new Bundle(); Bundle bobPayMetaData = new Bundle();
bobPayMetaData.putString( bobPayMetaData.putString(
AndroidPaymentAppFinder.META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME, AndroidPaymentAppFinder.META_DATA_NAME_OF_DEFAULT_PAYMENT_METHOD_NAME,
"https://bobpay.com"); "basic-card");
bobPayMetaData.putInt(AndroidPaymentAppFinder.META_DATA_NAME_OF_PAYMENT_METHOD_NAMES, 2); bobPayMetaData.putInt(AndroidPaymentAppFinder.META_DATA_NAME_OF_PAYMENT_METHOD_NAMES, 2);
bobPay.activityInfo.metaData = bobPayMetaData; bobPay.activityInfo.metaData = bobPayMetaData;
activities.add(bobPay); activities.add(bobPay);
...@@ -216,11 +216,11 @@ public class AndroidPaymentAppFinderTest { ...@@ -216,11 +216,11 @@ public class AndroidPaymentAppFinderTest {
Mockito.when(packageManagerDelegate.getStringArrayResourceForApplication( Mockito.when(packageManagerDelegate.getStringArrayResourceForApplication(
ArgumentMatchers.eq(alicePay.activityInfo.applicationInfo), ArgumentMatchers.eq(alicePay.activityInfo.applicationInfo),
ArgumentMatchers.eq(1))) ArgumentMatchers.eq(1)))
.thenReturn(new String[] {"https://alicepay.com", "basic-card"}); .thenReturn(new String[] {"https://alicepay.com"});
Mockito.when(packageManagerDelegate.getStringArrayResourceForApplication( Mockito.when(packageManagerDelegate.getStringArrayResourceForApplication(
ArgumentMatchers.eq(bobPay.activityInfo.applicationInfo), ArgumentMatchers.eq(bobPay.activityInfo.applicationInfo),
ArgumentMatchers.eq(2))) ArgumentMatchers.eq(2)))
.thenReturn(new String[] {"https://bobpay.com", "basic-card"}); .thenReturn(new String[] {"https://bobpay.com"});
Set<String> methodNames = new HashSet<>(); Set<String> methodNames = new HashSet<>();
methodNames.add("basic-card"); methodNames.add("basic-card");
......
...@@ -17,6 +17,7 @@ import org.robolectric.RobolectricTestRunner; ...@@ -17,6 +17,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.chromium.chrome.browser.payments.PaymentManifestVerifier.ManifestVerifyCallback; import org.chromium.chrome.browser.payments.PaymentManifestVerifier.ManifestVerifyCallback;
import org.chromium.chrome.browser.payments.PaymentManifestWebDataService.PaymentManifestWebDataServiceCallback;
import org.chromium.components.payments.PaymentManifestDownloader; import org.chromium.components.payments.PaymentManifestDownloader;
import org.chromium.components.payments.PaymentManifestParser; import org.chromium.components.payments.PaymentManifestParser;
import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContents;
...@@ -24,8 +25,8 @@ import org.chromium.payments.mojom.WebAppManifestSection; ...@@ -24,8 +25,8 @@ import org.chromium.payments.mojom.WebAppManifestSection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.HashSet;
import java.util.List; import java.util.Set;
/** A test for the verifier of a payment app manifest. */ /** A test for the verifier of a payment app manifest. */
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
...@@ -34,7 +35,7 @@ public class PaymentManifestVerifierTest { ...@@ -34,7 +35,7 @@ public class PaymentManifestVerifierTest {
private final URI mMethodName; private final URI mMethodName;
private final ResolveInfo mAlicePay; private final ResolveInfo mAlicePay;
private final ResolveInfo mBobPay; private final ResolveInfo mBobPay;
private final List<ResolveInfo> mMatchingApps; private final Set<ResolveInfo> mMatchingApps;
private final PaymentManifestDownloader mDownloader; private final PaymentManifestDownloader mDownloader;
private final PaymentManifestWebDataService mWebDataService; private final PaymentManifestWebDataService mWebDataService;
private final PaymentManifestParser mParser; private final PaymentManifestParser mParser;
...@@ -52,7 +53,7 @@ public class PaymentManifestVerifierTest { ...@@ -52,7 +53,7 @@ public class PaymentManifestVerifierTest {
mBobPay.activityInfo = new ActivityInfo(); mBobPay.activityInfo = new ActivityInfo();
mBobPay.activityInfo.packageName = "com.bobpay.app"; mBobPay.activityInfo.packageName = "com.bobpay.app";
mMatchingApps = new ArrayList<>(); mMatchingApps = new HashSet<>();
mMatchingApps.add(mAlicePay); mMatchingApps.add(mAlicePay);
mMatchingApps.add(mBobPay); mMatchingApps.add(mBobPay);
...@@ -75,7 +76,8 @@ public class PaymentManifestVerifierTest { ...@@ -75,7 +76,8 @@ public class PaymentManifestVerifierTest {
}; };
mWebDataService = Mockito.mock(PaymentManifestWebDataService.class); mWebDataService = Mockito.mock(PaymentManifestWebDataService.class);
Mockito.when(mWebDataService.getPaymentMethodManifest(Mockito.any(), Mockito.any())) Mockito.when(mWebDataService.getPaymentMethodManifest(Mockito.any(String.class),
Mockito.any(PaymentManifestWebDataServiceCallback.class)))
.thenReturn(false); .thenReturn(false);
mParser = new PaymentManifestParser() { mParser = new PaymentManifestParser() {
...@@ -128,8 +130,8 @@ public class PaymentManifestVerifierTest { ...@@ -128,8 +130,8 @@ public class PaymentManifestVerifierTest {
@Test @Test
public void testUnableToDownloadPaymentMethodManifest() { public void testUnableToDownloadPaymentMethodManifest() {
PaymentManifestVerifier verifier = new PaymentManifestVerifier( PaymentManifestVerifier verifier = new PaymentManifestVerifier(mMethodName, mMatchingApps,
mMethodName, mMatchingApps, mWebDataService, new PaymentManifestDownloader() { null /* supportedOrigins */, mWebDataService, new PaymentManifestDownloader() {
@Override @Override
public void initialize(WebContents webContents) {} public void initialize(WebContents webContents) {}
...@@ -145,13 +147,14 @@ public class PaymentManifestVerifierTest { ...@@ -145,13 +147,14 @@ public class PaymentManifestVerifierTest {
verifier.verify(); verifier.verify();
Mockito.verify(mCallback).onInvalidManifest(mMethodName); Mockito.verify(mCallback, Mockito.never())
.onValidDefaultPaymentApp(Mockito.any(URI.class), Mockito.any(ResolveInfo.class));
} }
@Test @Test
public void testUnableToDownloadWebAppManifest() { public void testUnableToDownloadWebAppManifest() {
PaymentManifestVerifier verifier = new PaymentManifestVerifier( PaymentManifestVerifier verifier = new PaymentManifestVerifier(mMethodName, mMatchingApps,
mMethodName, mMatchingApps, mWebDataService, new PaymentManifestDownloader() { null /* supportedOrigins */, mWebDataService, new PaymentManifestDownloader() {
@Override @Override
public void initialize(WebContents webContents) {} public void initialize(WebContents webContents) {}
...@@ -172,14 +175,17 @@ public class PaymentManifestVerifierTest { ...@@ -172,14 +175,17 @@ public class PaymentManifestVerifierTest {
verifier.verify(); verifier.verify();
Mockito.verify(mCallback).onInvalidManifest(mMethodName); Mockito.verify(mCallback, Mockito.never())
Mockito.verify(mCallback).onVerifyFinished(verifier); .onValidDefaultPaymentApp(Mockito.any(URI.class), Mockito.any(ResolveInfo.class));
Mockito.verify(mCallback).onFinishedVerification();
Mockito.verify(mCallback).onFinishedUsingResources();
} }
@Test @Test
public void testUnableToParsePaymentMethodManifest() { public void testUnableToParsePaymentMethodManifest() {
PaymentManifestVerifier verifier = new PaymentManifestVerifier(mMethodName, mMatchingApps, PaymentManifestVerifier verifier = new PaymentManifestVerifier(mMethodName, mMatchingApps,
mWebDataService, mDownloader, new PaymentManifestParser() { null /* supportedOrigins */, mWebDataService,
mDownloader, new PaymentManifestParser() {
@Override @Override
public void parsePaymentMethodManifest( public void parsePaymentMethodManifest(
String content, ManifestParseCallback callback) { String content, ManifestParseCallback callback) {
...@@ -189,14 +195,17 @@ public class PaymentManifestVerifierTest { ...@@ -189,14 +195,17 @@ public class PaymentManifestVerifierTest {
verifier.verify(); verifier.verify();
Mockito.verify(mCallback).onInvalidManifest(mMethodName); Mockito.verify(mCallback, Mockito.never())
Mockito.verify(mCallback).onVerifyFinished(verifier); .onValidDefaultPaymentApp(Mockito.any(URI.class), Mockito.any(ResolveInfo.class));
Mockito.verify(mCallback).onFinishedVerification();
Mockito.verify(mCallback).onFinishedUsingResources();
} }
@Test @Test
public void testUnableToParseWebAppManifest() { public void testUnableToParseWebAppManifest() {
PaymentManifestVerifier verifier = new PaymentManifestVerifier(mMethodName, mMatchingApps, PaymentManifestVerifier verifier = new PaymentManifestVerifier(mMethodName, mMatchingApps,
mWebDataService, mDownloader, new PaymentManifestParser() { null /* supportedOrigins */, mWebDataService,
mDownloader, new PaymentManifestParser() {
@Override @Override
public void parsePaymentMethodManifest( public void parsePaymentMethodManifest(
String content, ManifestParseCallback callback) { String content, ManifestParseCallback callback) {
...@@ -218,20 +227,24 @@ public class PaymentManifestVerifierTest { ...@@ -218,20 +227,24 @@ public class PaymentManifestVerifierTest {
verifier.verify(); verifier.verify();
Mockito.verify(mCallback).onInvalidManifest(mMethodName); Mockito.verify(mCallback, Mockito.never())
Mockito.verify(mCallback).onVerifyFinished(verifier); .onValidDefaultPaymentApp(Mockito.any(URI.class), Mockito.any(ResolveInfo.class));
Mockito.verify(mCallback).onFinishedVerification();
Mockito.verify(mCallback).onFinishedUsingResources();
} }
@Test @Test
public void testBobPayAllowed() { public void testBobPayAllowed() {
PaymentManifestVerifier verifier = new PaymentManifestVerifier(mMethodName, mMatchingApps, PaymentManifestVerifier verifier =
mWebDataService, mDownloader, mParser, mPackageManagerDelegate, mCallback); new PaymentManifestVerifier(mMethodName, mMatchingApps, null /* supportedOrigins */,
mWebDataService, mDownloader, mParser, mPackageManagerDelegate, mCallback);
verifier.verify(); verifier.verify();
Mockito.verify(mCallback).onInvalidPaymentApp(mMethodName, mAlicePay); Mockito.verify(mCallback, Mockito.never()).onValidDefaultPaymentApp(mMethodName, mAlicePay);
Mockito.verify(mCallback).onValidPaymentApp(mMethodName, mBobPay); Mockito.verify(mCallback).onValidDefaultPaymentApp(mMethodName, mBobPay);
Mockito.verify(mCallback).onVerifyFinished(verifier); Mockito.verify(mCallback).onFinishedVerification();
Mockito.verify(mCallback).onFinishedUsingResources();
} }
private class CountingParser extends PaymentManifestParser { private class CountingParser extends PaymentManifestParser {
...@@ -281,13 +294,16 @@ public class PaymentManifestVerifierTest { ...@@ -281,13 +294,16 @@ public class PaymentManifestVerifierTest {
} }
}; };
PaymentManifestVerifier verifier = new PaymentManifestVerifier(mMethodName, mMatchingApps, PaymentManifestVerifier verifier =
mWebDataService, downloader, parser, mPackageManagerDelegate, mCallback); new PaymentManifestVerifier(mMethodName, mMatchingApps, null /* supportedOrigins */,
mWebDataService, downloader, parser, mPackageManagerDelegate, mCallback);
verifier.verify(); verifier.verify();
Mockito.verify(mCallback).onInvalidManifest(mMethodName); Mockito.verify(mCallback, Mockito.never())
Mockito.verify(mCallback).onVerifyFinished(verifier); .onValidDefaultPaymentApp(Mockito.any(URI.class), Mockito.any(ResolveInfo.class));
Mockito.verify(mCallback).onFinishedVerification();
Mockito.verify(mCallback).onFinishedUsingResources();
Assert.assertEquals(1, downloader.mDownloadWebAppManifestCounter); Assert.assertEquals(1, downloader.mDownloadWebAppManifestCounter);
Assert.assertEquals(0, parser.mParseWebAppManifestCounter); Assert.assertEquals(0, parser.mParseWebAppManifestCounter);
} }
...@@ -331,13 +347,16 @@ public class PaymentManifestVerifierTest { ...@@ -331,13 +347,16 @@ public class PaymentManifestVerifierTest {
} }
}; };
PaymentManifestVerifier verifier = new PaymentManifestVerifier(mMethodName, mMatchingApps, PaymentManifestVerifier verifier =
mWebDataService, downloader, parser, mPackageManagerDelegate, mCallback); new PaymentManifestVerifier(mMethodName, mMatchingApps, null /* supportedOrigins */,
mWebDataService, downloader, parser, mPackageManagerDelegate, mCallback);
verifier.verify(); verifier.verify();
Mockito.verify(mCallback).onInvalidManifest(mMethodName); Mockito.verify(mCallback, Mockito.never())
Mockito.verify(mCallback).onVerifyFinished(verifier); .onValidDefaultPaymentApp(Mockito.any(URI.class), Mockito.any(ResolveInfo.class));
Mockito.verify(mCallback).onFinishedVerification();
Mockito.verify(mCallback).onFinishedUsingResources();
Assert.assertEquals(1, downloader.mDownloadWebAppManifestCounter); Assert.assertEquals(1, downloader.mDownloadWebAppManifestCounter);
Assert.assertEquals(1, parser.mParseWebAppManifestCounter); Assert.assertEquals(1, parser.mParseWebAppManifestCounter);
} }
......
{
"supported_origins": ["https://alicepay.com"]
}
HTTP/1.1 200 OK
Link: <payment-manifest.json>; rel="payment-method-manifest"
{
"name": "HenryPay",
"related_applications": [{
"platform": "play",
"id": "com.henrypay",
"min_version": "1",
"fingerprints": [{
"type": "sha256_cert",
"value": "74:2C:5F:7D:3E:A0:CE:D7:C9:5A:5C:07:55:FD:8C:59:9B:B0:1E:1B:F2:6E:3E:32:3F:23:AB:04:42:2A:75:E3",
"comment": "This fingperint is SHA256 of '55555555551111111111'"
}]
}]
}
{
"default_applications": ["https://henrypay.com/app.json"],
"supported_origins": "*"
}
HTTP/1.1 200 OK
Link: <payment-manifest.json>; rel="payment-method-manifest"
{
"name": "IkePay",
"related_applications": [{
"platform": "play",
"id": "com.ikepay",
"min_version": "1",
"fingerprints": [{
"type": "sha256_cert",
"value": "46:D0:55:48:2F:BB:2A:E0:CC:BC:60:21:90:70:08:4C:FB:FD:4D:B7:DF:08:B7:FE:4D:AE:3E:0B:1D:62:D7:B9",
"comment": "This fingperint is SHA256 of '66666666661111111111'"
}]
}]
}
{
"default_applications": ["https://ikepay.com/app.json"],
"supported_origins": ["https://alicepay.com"]
}
HTTP/1.1 200 OK
Link: <payment-manifest.json>; rel="payment-method-manifest"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment