diff --git a/docs/app/components/content/PayPalDemo.vue b/docs/app/components/content/PayPalDemo.vue new file mode 100644 index 00000000..f299c6fb --- /dev/null +++ b/docs/app/components/content/PayPalDemo.vue @@ -0,0 +1,32 @@ + + + diff --git a/docs/content/scripts/payments/paypal.md b/docs/content/scripts/payments/paypal.md new file mode 100644 index 00000000..8709fa5a --- /dev/null +++ b/docs/content/scripts/payments/paypal.md @@ -0,0 +1,80 @@ +--- +title: PayPal +description: Use PayPal in your Nuxt app. +links: + - label: useScriptPayPal + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/registry/paypal.ts + size: xs + - label: "" + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/components/ScriptPayPalButtons.vue + size: xs + - label: "" + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/components/ScriptPayPalMarks.vue + size: xs + - label: "" + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/components/ScriptPayPalMessages.vue + size: xs +--- + +[PayPal](https://www.paypal.com) is a popular payment gateway that allows you to accept payments online. + +Nuxt Scripts provides multiple PayPal features: +- `useScriptPayPal` composable which loads the script `https://www.paypal.com/sdk/js`. +- `ScriptPayPalButtons` component that allows you to embed [PayPal Buttons](https://developer.paypal.com/sdk/js/reference/#buttons) on your site. +- `ScriptPayPalMarks` component that allows you to embed [PayPal Marks](https://developer.paypal.com/sdk/js/reference/#marks) on your site. +- `ScriptPayPalMessages` component that allows you to embed [PayPal Messages](https://developer.paypal.com/studio/checkout/pay-later/us/customize/reference) on your site. + +## Types + +To use the PayPal with full TypeScript support, you will need +to install the `@paypal/paypal-js` dependency. + +```bash +pnpm add -D @paypal/paypal-js +``` +### Demo + +::code-group + +:pay-pal-demo{label="Output"} + +```vue [Input] + + + +``` + +:: diff --git a/package.json b/package.json index 89fc41e5..6bbcccdb 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ }, "peerDependencies": { "@stripe/stripe-js": "^7.0.0", + "@paypal/paypal-js": "^8.1.2", "@types/google.maps": "^3.58.1", "@types/vimeo__player": "^2.18.3", "@types/youtube": "^0.1.0", @@ -82,6 +83,9 @@ "@stripe/stripe-js": { "optional": true }, + "@paypal/paypal-js": { + "optional": true + }, "@types/google.maps": { "optional": true }, diff --git a/playground/pages/index.vue b/playground/pages/index.vue index 2f9e45fe..fe059d1d 100644 --- a/playground/pages/index.vue +++ b/playground/pages/index.vue @@ -54,6 +54,10 @@ const thirdParties = [ name: 'Stripe', path: '/third-parties/stripe/nuxt-scripts', }, + { + name: 'PayPal', + path: '/third-parties/paypal/nuxt-scripts', + }, { name: 'Segment', path: '/third-parties/segment', diff --git a/playground/pages/third-parties/paypal/nuxt-scripts.vue b/playground/pages/third-parties/paypal/nuxt-scripts.vue new file mode 100644 index 00000000..f299c6fb --- /dev/null +++ b/playground/pages/third-parties/paypal/nuxt-scripts.vue @@ -0,0 +1,32 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6300f566..ee11aa83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@nuxt/kit': specifier: 'catalog:' version: 4.0.3(magicast@0.3.5) + '@paypal/paypal-js': + specifier: ^8.1.2 + version: 8.2.0 '@stripe/stripe-js': specifier: ^7.0.0 version: 7.8.0 @@ -1571,6 +1574,9 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@paypal/paypal-js@8.2.0': + resolution: {integrity: sha512-hLg5wNORW3WiyMiRNJOm6cN2IqjPlClpxd971bEdm0LNpbbejQZYtesb0/0arTnySSbGcxg7MxjkZ/N5Z5qBNQ==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -5749,6 +5755,9 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + promise-polyfill@8.3.0: + resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -9063,6 +9072,10 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 + '@paypal/paypal-js@8.2.0': + dependencies: + promise-polyfill: 8.3.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -14074,6 +14087,8 @@ snapshots: process@0.11.10: {} + promise-polyfill@8.3.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 diff --git a/src/registry.ts b/src/registry.ts index c4615b2f..aa56ce78 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -205,6 +205,16 @@ export async function registry(resolve?: (path: string, opts?: ResolvePathOption from: await resolve('./runtime/registry/lemon-squeezy'), }, }, + { + label: 'PayPal', + src: false, // should not be bundled + category: 'payments', + logo: ``, + import: { + name: 'useSciptPayPal', + from: await resolve('./runtime/registry/paypal'), + }, + }, // content { label: 'Vimeo Player', diff --git a/src/runtime/components/ScriptPayPalButtons.vue b/src/runtime/components/ScriptPayPalButtons.vue new file mode 100644 index 00000000..736286f8 --- /dev/null +++ b/src/runtime/components/ScriptPayPalButtons.vue @@ -0,0 +1,187 @@ + + + diff --git a/src/runtime/components/ScriptPayPalMarks.vue b/src/runtime/components/ScriptPayPalMarks.vue new file mode 100644 index 00000000..a77661ff --- /dev/null +++ b/src/runtime/components/ScriptPayPalMarks.vue @@ -0,0 +1,102 @@ + + + diff --git a/src/runtime/components/ScriptPayPalMessages.vue b/src/runtime/components/ScriptPayPalMessages.vue new file mode 100644 index 00000000..458cdd04 --- /dev/null +++ b/src/runtime/components/ScriptPayPalMessages.vue @@ -0,0 +1,136 @@ + + + diff --git a/src/runtime/registry/paypal.ts b/src/runtime/registry/paypal.ts new file mode 100644 index 00000000..3ca6ceff --- /dev/null +++ b/src/runtime/registry/paypal.ts @@ -0,0 +1,109 @@ +import { withQuery } from 'ufo' +import type { PayPalNamespace } from '@paypal/paypal-js' +import { useRegistryScript } from '../utils' +import { object, string, optional, array, union, boolean } from '#nuxt-scripts-validator' +import type { RegistryScriptInput } from '#nuxt-scripts/types' + +export interface PayPalApi { + paypal: PayPalNamespace +} + +declare global { + interface Window extends PayPalApi { + } +} + +export const PayPalOptions = object({ + clientId: string(), + buyerCountry: optional(string()), + commit: optional(string()), + components: optional(union([string(), array(string())])), + currency: optional(string()), + debug: optional(union([string(), boolean()])), + disableFunding: optional(union([string(), array(string())])), + enableFunding: optional(union([string(), array(string())])), + integrationDate: optional(string()), + intent: optional(string()), + locale: optional(string()), + /** + * loadScript() supports an array for merchantId, even though + * merchant-id technically may not contain multiple values. + * For an array with a length of > 1 it automatically sets + * merchantId to "*" and moves the actual values to dataMerchantId + */ + merchantId: optional(union([string(), array(string())])), + partnerAttributionId: optional(string()), + vault: optional(union([string(), boolean()])), + // own props + sandbox: optional(boolean()), +}) + +export type PayPalInput = RegistryScriptInput + +export function useScriptPayPal(_options?: PayPalInput) { + return useRegistryScript('paypal', (options) => { + let dataMerchantId = undefined + + if (Array.isArray(options?.merchantId) && options?.merchantId.length > 1) { + dataMerchantId = JSON.stringify(options.merchantId) + options.merchantId = '*' + } + + if (Array.isArray(options?.components)) { + options.components = options.components.join(',') + } + + if (Array.isArray(options?.disableFunding)) { + options.disableFunding = options.disableFunding.join(',') + } + + if (Array.isArray(options?.enableFunding)) { + options.enableFunding = options.enableFunding.join(',') + } + + if (options?.sandbox === undefined) { + options.sandbox = import.meta.dev + } + + let components = ['buttons', 'messages', 'marks', 'card-fields', 'funding-eligibility'].join(',') + + if (options.components) { + if (Array.isArray(options.components)) { + components = options.components.join(',') + } + else { + components = options.components + } + } + + return { + scriptInput: { + 'src': withQuery(options.sandbox ? 'https://www.sandbox.paypal.com/sdk/js' : 'https://www.paypal.com/sdk/js', { + 'client-id': options.clientId, + 'buyer-country': options.buyerCountry, + 'commit': options.commit, + 'components': components, + 'currency': options.currency, + 'debug': options.debug, + 'disable-funding': options.disableFunding, + 'enable-funding': options.enableFunding, + 'integration-date': options.integrationDate, + 'intent': options.intent, + 'locale': options.locale, + 'vault': options.vault, + }), + 'data-merchant-id': dataMerchantId, + 'data-partner-attribution-id': options.partnerAttributionId, // TODO: maybe nuxt specific default + }, + schema: import.meta.dev ? PayPalOptions : undefined, + // trigger: 'client', + scriptOptions: { + use() { + return { + paypal: window.paypal, + } + }, + }, + } + }, _options) +}