mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-30 05:00:51 +08:00
Add GooglePay Manager
This commit is contained in:
parent
e87ab7362c
commit
dc81b76f17
6 changed files with 270 additions and 237 deletions
|
@ -10,6 +10,7 @@
|
|||
"Edge >= 14"
|
||||
],
|
||||
"dependencies": {
|
||||
"@paypal/paypal-js": "^6.0.0",
|
||||
"core-js": "^3.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
227
modules/ppcp-googlepay/resources/js/GooglepayManager.js
Normal file
227
modules/ppcp-googlepay/resources/js/GooglepayManager.js
Normal file
|
@ -0,0 +1,227 @@
|
|||
|
||||
class GooglepayManager {
|
||||
|
||||
constructor(buttonConfig) {
|
||||
|
||||
this.buttonConfig = buttonConfig;
|
||||
|
||||
this.allowedPaymentMethods = null;
|
||||
this.merchantInfo = null;
|
||||
this.googlePayConfig = null;
|
||||
|
||||
this.isReadyToPayRequest = null;
|
||||
this.baseCardPaymentMethod = null;
|
||||
}
|
||||
|
||||
init() {
|
||||
(async () => {
|
||||
let cfg = await this.config();
|
||||
console.log('googlePayConfig', this.googlePayConfig);
|
||||
|
||||
this.allowedPaymentMethods = cfg.allowedPaymentMethods;
|
||||
|
||||
this.isReadyToPayRequest = this.buildReadyToPayRequest(this.allowedPaymentMethods, this.googlePayConfig);
|
||||
console.log('googleIsReadyToPayRequest', this.isReadyToPayRequest);
|
||||
|
||||
this.baseCardPaymentMethod = this.allowedPaymentMethods[0];
|
||||
|
||||
this.load();
|
||||
})();
|
||||
}
|
||||
|
||||
async config() {
|
||||
console.log('getGooglePayConfig');
|
||||
console.log('allowedPaymentMethods', this.allowedPaymentMethods);
|
||||
console.log('merchantInfo', this.merchantInfo);
|
||||
|
||||
if (this.allowedPaymentMethods == null || this.merchantInfo == null) {
|
||||
this.googlePayConfig = await paypal.Googlepay().config();
|
||||
|
||||
console.log('const googlePayConfig', this.googlePayConfig);
|
||||
|
||||
this.allowedPaymentMethods = this.googlePayConfig.allowedPaymentMethods;
|
||||
this.merchantInfo = this.googlePayConfig.merchantInfo;
|
||||
}
|
||||
return {
|
||||
allowedPaymentMethods: this.allowedPaymentMethods,
|
||||
merchantInfo: this.merchantInfo,
|
||||
};
|
||||
}
|
||||
|
||||
buildReadyToPayRequest(allowedPaymentMethods, baseRequest) {
|
||||
console.log('allowedPaymentMethods', allowedPaymentMethods);
|
||||
|
||||
return Object.assign({}, baseRequest, {
|
||||
allowedPaymentMethods: allowedPaymentMethods,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Google PaymentsClient after Google-hosted JavaScript has loaded
|
||||
* Display a Google Pay payment button after confirmation of the viewer's ability to pay.
|
||||
*/
|
||||
load() {
|
||||
console.log('onGooglePayLoaded');
|
||||
|
||||
const paymentsClient = this.client();
|
||||
paymentsClient.isReadyToPay(this.isReadyToPayRequest)
|
||||
.then((response) => {
|
||||
if (response.result) {
|
||||
this.addButton();
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
client() {
|
||||
if (window.googlePayClient) {
|
||||
return window.googlePayClient;
|
||||
}
|
||||
|
||||
window.googlePayClient = new google.payments.api.PaymentsClient({
|
||||
environment: 'TEST', // Use 'PRODUCTION' for real transactions
|
||||
// add merchant info maybe
|
||||
paymentDataCallbacks: {
|
||||
//onPaymentDataChanged: onPaymentDataChanged,
|
||||
onPaymentAuthorized: this.onPaymentAuthorized,
|
||||
}
|
||||
});
|
||||
|
||||
return window.googlePayClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Google Pay purchase button
|
||||
*/
|
||||
addButton() {
|
||||
console.log('addGooglePayButton');
|
||||
|
||||
const paymentsClient = this.client();
|
||||
const button =
|
||||
paymentsClient.createButton({
|
||||
onClick: this.onButtonClick.bind(this),
|
||||
allowedPaymentMethods: [this.baseCardPaymentMethod]
|
||||
});
|
||||
jQuery(this.buttonConfig.button.wrapper).append(button);
|
||||
}
|
||||
|
||||
//------------------------
|
||||
// Button click
|
||||
//------------------------
|
||||
|
||||
/**
|
||||
* Show Google Pay payment sheet when Google Pay payment button is clicked
|
||||
*/
|
||||
async onButtonClick() {
|
||||
console.log('onGooglePaymentButtonClicked');
|
||||
|
||||
const paymentDataRequest = await this.paymentDataRequest();
|
||||
const paymentsClient = this.client();
|
||||
paymentsClient.loadPaymentData(paymentDataRequest);
|
||||
}
|
||||
|
||||
/* Note: the `googlePayConfig` object in this request is the response from `paypal.Googlepay().config()` */
|
||||
async paymentDataRequest() {
|
||||
let baseRequest = {
|
||||
apiVersion: 2,
|
||||
apiVersionMinor: 0
|
||||
}
|
||||
|
||||
const googlePayConfig = await paypal.Googlepay().config();
|
||||
const paymentDataRequest = Object.assign({}, baseRequest);
|
||||
paymentDataRequest.allowedPaymentMethods = googlePayConfig.allowedPaymentMethods;
|
||||
paymentDataRequest.transactionInfo = this.transactionInfo();
|
||||
paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo;
|
||||
paymentDataRequest.callbackIntents = ["PAYMENT_AUTHORIZATION"];
|
||||
return paymentDataRequest;
|
||||
}
|
||||
|
||||
transactionInfo() {
|
||||
return {
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
totalPriceStatus: 'FINAL',
|
||||
totalPrice: '2.01' // Your amount
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------
|
||||
// Payment process
|
||||
//------------------------
|
||||
|
||||
onPaymentAuthorized(paymentData) {
|
||||
console.log('onPaymentAuthorized', paymentData);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.processPayment(paymentData)
|
||||
.then(function (data) {
|
||||
resolve({ transactionState: "SUCCESS" });
|
||||
})
|
||||
.catch(function (errDetails) {
|
||||
resolve({ transactionState: "ERROR" });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async processPayment(paymentData) {
|
||||
return new Promise(async function (resolve, reject) {
|
||||
try {
|
||||
// Create the order on your server
|
||||
const {id} = await fetch(`/orders`, {
|
||||
method: "POST",
|
||||
body: ''
|
||||
// You can use the "body" parameter to pass optional, additional order information, such as:
|
||||
// amount, and amount breakdown elements like tax, shipping, and handling
|
||||
// item data, such as sku, name, unit_amount, and quantity
|
||||
// shipping information, like name, address, and address type
|
||||
});
|
||||
|
||||
console.log('paypal.Googlepay().confirmOrder : paymentData', paymentData);
|
||||
const confirmOrderResponse = await paypal.Googlepay().confirmOrder({
|
||||
orderId: id,
|
||||
paymentMethodData: paymentData.paymentMethodData
|
||||
});
|
||||
console.log('paypal.Googlepay().confirmOrder : confirmOrderResponse', confirmOrderResponse);
|
||||
|
||||
/** Capture the Order on your Server */
|
||||
if(confirmOrderResponse.status === "APPROVED"){
|
||||
const response = await fetch(`/capture/${id}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}).then(res => res.json());
|
||||
if(response.capture.status === "COMPLETED")
|
||||
resolve({transactionState: 'SUCCESS'});
|
||||
else
|
||||
resolve({
|
||||
transactionState: 'ERROR',
|
||||
error: {
|
||||
intent: 'PAYMENT_AUTHORIZATION',
|
||||
message: 'TRANSACTION FAILED',
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
transactionState: 'ERROR',
|
||||
error: {
|
||||
intent: 'PAYMENT_AUTHORIZATION',
|
||||
message: 'TRANSACTION FAILED',
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch(err) {
|
||||
resolve({
|
||||
transactionState: 'ERROR',
|
||||
error: {
|
||||
intent: 'PAYMENT_AUTHORIZATION',
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default GooglepayManager;
|
|
@ -1,4 +1,6 @@
|
|||
import {loadCustomScript} from "@paypal/paypal-js";
|
||||
import {loadPaypalScript} from "../../../ppcp-button/resources/js/modules/Helper/ScriptLoading";
|
||||
import GooglepayManager from "./GooglepayManager";
|
||||
|
||||
(function ({
|
||||
buttonConfig,
|
||||
|
@ -7,226 +9,8 @@ import {loadPaypalScript} from "../../../ppcp-button/resources/js/modules/Helper
|
|||
}) {
|
||||
|
||||
const bootstrap = function () {
|
||||
|
||||
let allowedPaymentMethods = null;
|
||||
let merchantInfo = null;
|
||||
let googlePayConfig = null;
|
||||
|
||||
let isReadyToPayRequest = null;
|
||||
let baseCardPaymentMethod = null;
|
||||
|
||||
/* Configure your site's support for payment methods supported by the Google Pay */
|
||||
function getGoogleIsReadyToPayRequest(allowedPaymentMethods, baseRequest) {
|
||||
console.log('allowedPaymentMethods', allowedPaymentMethods);
|
||||
|
||||
return Object.assign({}, baseRequest, {
|
||||
allowedPaymentMethods: allowedPaymentMethods,
|
||||
});
|
||||
}
|
||||
|
||||
/* Fetch Default Config from PayPal via PayPal SDK */
|
||||
async function getGooglePayConfig() {
|
||||
console.log('getGooglePayConfig');
|
||||
|
||||
console.log('allowedPaymentMethods', allowedPaymentMethods);
|
||||
console.log('merchantInfo', merchantInfo);
|
||||
|
||||
if (allowedPaymentMethods == null || merchantInfo == null) {
|
||||
googlePayConfig = await paypal.Googlepay().config();
|
||||
|
||||
console.log('const googlePayConfig', googlePayConfig);
|
||||
|
||||
allowedPaymentMethods = googlePayConfig.allowedPaymentMethods;
|
||||
merchantInfo = googlePayConfig.merchantInfo;
|
||||
}
|
||||
return {
|
||||
allowedPaymentMethods,
|
||||
merchantInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Google PaymentsClient after Google-hosted JavaScript has loaded
|
||||
* Display a Google Pay payment button after confirmation of the viewer's ability to pay.
|
||||
*/
|
||||
function onGooglePayLoaded() {
|
||||
console.log('onGooglePayLoaded');
|
||||
|
||||
const paymentsClient = getGooglePaymentsClient();
|
||||
paymentsClient.isReadyToPay(isReadyToPayRequest)
|
||||
.then(function(response) {
|
||||
if (response.result) {
|
||||
addGooglePayButton();
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Google Pay purchase button
|
||||
*/
|
||||
function addGooglePayButton() {
|
||||
console.log('addGooglePayButton');
|
||||
|
||||
const paymentsClient = getGooglePaymentsClient();
|
||||
const button =
|
||||
paymentsClient.createButton({
|
||||
onClick: onGooglePaymentButtonClicked /* To be defined later */,
|
||||
allowedPaymentMethods: [baseCardPaymentMethod]
|
||||
});
|
||||
jQuery(buttonConfig.button.wrapper).append(button);
|
||||
}
|
||||
|
||||
/* Note: the `googlePayConfig` object in this request is the response from `paypal.Googlepay().config()` */
|
||||
async function getGooglePaymentDataRequest() {
|
||||
let baseRequest = {
|
||||
apiVersion: 2,
|
||||
apiVersionMinor: 0
|
||||
}
|
||||
|
||||
const googlePayConfig = await paypal.Googlepay().config();
|
||||
const paymentDataRequest = Object.assign({}, baseRequest);
|
||||
paymentDataRequest.allowedPaymentMethods = googlePayConfig.allowedPaymentMethods;
|
||||
paymentDataRequest.transactionInfo = getGoogleTransactionInfo();
|
||||
paymentDataRequest.merchantInfo = googlePayConfig.merchantInfo;
|
||||
paymentDataRequest.callbackIntents = ["PAYMENT_AUTHORIZATION"];
|
||||
return paymentDataRequest;
|
||||
}
|
||||
|
||||
function getGoogleTransactionInfo(){
|
||||
return {
|
||||
countryCode: 'US',
|
||||
currencyCode: 'USD',
|
||||
totalPriceStatus: 'FINAL',
|
||||
totalPrice: '2.01' // Your amount
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Google Pay payment sheet when Google Pay payment button is clicked
|
||||
*/
|
||||
async function onGooglePaymentButtonClicked() {
|
||||
console.log('onGooglePaymentButtonClicked');
|
||||
|
||||
const paymentDataRequest = await getGooglePaymentDataRequest();
|
||||
const paymentsClient = getGooglePaymentsClient();
|
||||
paymentsClient.loadPaymentData(paymentDataRequest);
|
||||
}
|
||||
|
||||
function onPaymentAuthorized(paymentData) {
|
||||
console.log('onPaymentAuthorized', paymentData);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
processPayment(paymentData)
|
||||
.then(function (data) {
|
||||
resolve({ transactionState: "SUCCESS" });
|
||||
})
|
||||
.catch(function (errDetails) {
|
||||
resolve({ transactionState: "ERROR" });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onPaymentDataChanged() {
|
||||
console.log('onPaymentDataChanged');
|
||||
}
|
||||
|
||||
async function processPayment(paymentData) {
|
||||
return new Promise(async function (resolve, reject) {
|
||||
try {
|
||||
// Create the order on your server
|
||||
const {id} = await fetch(`/orders`, {
|
||||
method: "POST",
|
||||
body: ''
|
||||
// You can use the "body" parameter to pass optional, additional order information, such as:
|
||||
// amount, and amount breakdown elements like tax, shipping, and handling
|
||||
// item data, such as sku, name, unit_amount, and quantity
|
||||
// shipping information, like name, address, and address type
|
||||
});
|
||||
|
||||
console.log('paypal.Googlepay().confirmOrder : paymentData', paymentData);
|
||||
const confirmOrderResponse = await paypal.Googlepay().confirmOrder({
|
||||
orderId: id,
|
||||
paymentMethodData: paymentData.paymentMethodData
|
||||
});
|
||||
console.log('paypal.Googlepay().confirmOrder : confirmOrderResponse', confirmOrderResponse);
|
||||
|
||||
/** Capture the Order on your Server */
|
||||
if(confirmOrderResponse.status === "APPROVED"){
|
||||
const response = await fetch(`/capture/${id}`,
|
||||
{
|
||||
method: 'POST',
|
||||
}).then(res => res.json());
|
||||
if(response.capture.status === "COMPLETED")
|
||||
resolve({transactionState: 'SUCCESS'});
|
||||
else
|
||||
resolve({
|
||||
transactionState: 'ERROR',
|
||||
error: {
|
||||
intent: 'PAYMENT_AUTHORIZATION',
|
||||
message: 'TRANSACTION FAILED',
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
transactionState: 'ERROR',
|
||||
error: {
|
||||
intent: 'PAYMENT_AUTHORIZATION',
|
||||
message: 'TRANSACTION FAILED',
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch(err) {
|
||||
resolve({
|
||||
transactionState: 'ERROR',
|
||||
error: {
|
||||
intent: 'PAYMENT_AUTHORIZATION',
|
||||
message: err.message,
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Custom
|
||||
function getGooglePaymentsClient() {
|
||||
if (window.googlePayClient) {
|
||||
return window.googlePayClient;
|
||||
}
|
||||
|
||||
window.googlePayClient = new google.payments.api.PaymentsClient({
|
||||
environment: 'TEST', // Use 'PRODUCTION' for real transactions
|
||||
// add merchant info maybe
|
||||
paymentDataCallbacks: {
|
||||
//onPaymentDataChanged: onPaymentDataChanged,
|
||||
onPaymentAuthorized: onPaymentAuthorized,
|
||||
}
|
||||
});
|
||||
|
||||
return window.googlePayClient;
|
||||
}
|
||||
|
||||
//------------------------
|
||||
|
||||
setTimeout(async function () {
|
||||
let cfg = await getGooglePayConfig();
|
||||
console.log('googlePayConfig', googlePayConfig);
|
||||
|
||||
allowedPaymentMethods = cfg.allowedPaymentMethods;
|
||||
|
||||
isReadyToPayRequest = getGoogleIsReadyToPayRequest(allowedPaymentMethods, googlePayConfig);
|
||||
console.log('googleIsReadyToPayRequest', isReadyToPayRequest);
|
||||
|
||||
baseCardPaymentMethod = allowedPaymentMethods[0];
|
||||
|
||||
onGooglePayLoaded();
|
||||
|
||||
}, 2000);
|
||||
|
||||
|
||||
|
||||
const manager = new GooglepayManager(buttonConfig);
|
||||
manager.init();
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
|
@ -241,10 +25,26 @@ import {loadPaypalScript} from "../../../ppcp-button/resources/js/modules/Helper
|
|||
}
|
||||
|
||||
let bootstrapped = false;
|
||||
let paypalLoaded = false;
|
||||
let googlePayLoaded = false;
|
||||
|
||||
const tryToBoot = () => {
|
||||
if (!bootstrapped && paypalLoaded && googlePayLoaded) {
|
||||
bootstrapped = true;
|
||||
bootstrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Load GooglePay SDK
|
||||
loadCustomScript({ url: buttonConfig.sdk_url }).then(() => {
|
||||
googlePayLoaded = true;
|
||||
tryToBoot();
|
||||
});
|
||||
|
||||
// Load PayPal
|
||||
loadPaypalScript(ppcpConfig, () => {
|
||||
bootstrapped = true;
|
||||
bootstrap();
|
||||
paypalLoaded = true;
|
||||
tryToBoot();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
@ -18,12 +18,12 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
|||
return array(
|
||||
// TODO.
|
||||
|
||||
'googlepay.button' => static function ( ContainerInterface $container ): ButtonInterface {
|
||||
'googlepay.button' => static function ( ContainerInterface $container ): ButtonInterface {
|
||||
// TODO : check other statuses.
|
||||
|
||||
return new GooglepayButton(
|
||||
$container->get( 'googlepay.url' ),
|
||||
$container->get( 'googlepay.sdk_script_url' ),
|
||||
$container->get( 'googlepay.sdk_url' ),
|
||||
$container->get( 'ppcp.asset-version' ),
|
||||
$container->get( 'session.handler' ),
|
||||
$container->get( 'wcgateway.settings' ),
|
||||
|
@ -33,7 +33,7 @@ return array(
|
|||
);
|
||||
},
|
||||
|
||||
'googlepay.url' => static function ( ContainerInterface $container ): string {
|
||||
'googlepay.url' => static function ( ContainerInterface $container ): string {
|
||||
$path = realpath( __FILE__ );
|
||||
if ( false === $path ) {
|
||||
return '';
|
||||
|
@ -43,7 +43,8 @@ return array(
|
|||
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
},
|
||||
'googlepay.sdk_script_url' => static function ( ContainerInterface $container ): string {
|
||||
|
||||
'googlepay.sdk_url' => static function ( ContainerInterface $container ): string {
|
||||
return 'https://pay.google.com/gp/p/js/pay.js';
|
||||
},
|
||||
|
||||
|
|
|
@ -185,19 +185,10 @@ class GooglepayButton implements ButtonInterface {
|
|||
* Enqueues scripts/styles.
|
||||
*/
|
||||
public function enqueue(): void {
|
||||
wp_register_script(
|
||||
'wc-ppcp-googlepay-sdk',
|
||||
$this->sdk_url,
|
||||
array(),
|
||||
$this->version,
|
||||
true
|
||||
);
|
||||
wp_enqueue_script( 'wc-ppcp-googlepay-sdk' );
|
||||
|
||||
wp_register_script(
|
||||
'wc-ppcp-googlepay',
|
||||
untrailingslashit( $this->module_url ) . '/assets/js/boot.js',
|
||||
array( 'wc-ppcp-googlepay-sdk' ),
|
||||
array(),
|
||||
$this->version,
|
||||
true
|
||||
);
|
||||
|
@ -225,7 +216,8 @@ class GooglepayButton implements ButtonInterface {
|
|||
*/
|
||||
public function script_data(): array {
|
||||
return array(
|
||||
'button' => array(
|
||||
'sdk_url' => $this->sdk_url,
|
||||
'button' => array(
|
||||
'wrapper' => '#ppc-button-googlepay-container',
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1031,6 +1031,13 @@
|
|||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@paypal/paypal-js@^6.0.0":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-6.0.1.tgz#5d68d5863a5176383fee9424bc944231668fcffd"
|
||||
integrity sha512-bvYetmkg2GEC6onsUJQx1E9hdAJWff2bS3IPeiZ9Sh9U7h26/fIgMKm240cq/908sbSoDjHys75XXd8at9OpQA==
|
||||
dependencies:
|
||||
promise-polyfill "^8.3.0"
|
||||
|
||||
"@types/eslint-scope@^3.7.3":
|
||||
version "3.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
|
||||
|
@ -1868,6 +1875,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
|||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
promise-polyfill@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
|
||||
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue