diff --git a/package.json b/package.json index 5014836d..ec92ef45 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "@nuxt/module-builder": "^1.0.1", "@nuxt/scripts": "workspace:*", "@nuxt/test-utils": "3.19.2", + "@paypal/paypal-js": "^8.1.2", "@types/semver": "^7.7.0", "@typescript-eslint/typescript-estree": "^8.35.1", "acorn-loose": "^8.5.2", 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..38d1cd0c --- /dev/null +++ b/playground/pages/third-parties/paypal/nuxt-scripts.vue @@ -0,0 +1,35 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6c6cd48..4bbc007f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: '@nuxt/test-utils': specifier: 3.19.2 version: 3.19.2(happy-dom@18.0.1)(magicast@0.3.5)(playwright-core@1.53.2)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.10)(happy-dom@18.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0)) + '@paypal/paypal-js': + specifier: ^8.1.2 + version: 8.2.0 '@types/semver': specifier: ^7.7.0 version: 7.7.0 @@ -845,8 +848,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nuxt/cli-nightly@3.26.0-20250702-114812-ac2d954': - resolution: {integrity: sha512-0wk4nwuOGnPZG2IpQRP2vWB2awWo8N4Jm84W9+LUAhwDYa53BfW7d2OiyOmbov2nIbvCzI9mEFnqljjW2/fKXQ==} + '@nuxt/cli-nightly@3.26.2-20250715-082518-d2c200e': + resolution: {integrity: sha512-2QkFKV9vQOERlxGGKzvWolgkeUaPiLExwmOmFBUdAEgj7jCtIIr/7E3tjweKbMlsNF0va2mni9FeddLAtZDXDQ==} engines: {node: ^16.10.0 || >=18.0.0} hasBin: true @@ -1417,6 +1420,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'} @@ -2767,6 +2773,14 @@ packages: magicast: optional: true + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -5483,6 +5497,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'} @@ -7661,9 +7678,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nuxt/cli-nightly@3.26.0-20250702-114812-ac2d954(magicast@0.3.5)': + '@nuxt/cli-nightly@3.26.2-20250715-082518-d2c200e(magicast@0.3.5)': dependencies: - c12: 3.0.4(magicast@0.3.5) + c12: 3.1.0(magicast@0.3.5) citty: 0.1.6 clipboardy: 4.0.0 confbox: 0.2.2 @@ -8792,6 +8809,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 @@ -10359,6 +10380,23 @@ snapshots: optionalDependencies: magicast: 0.3.5 + c12@3.1.0(magicast@0.3.5): + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.4.2 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.2.0 + rc9: 2.1.2 + optionalDependencies: + magicast: 0.3.5 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -13001,7 +13039,7 @@ snapshots: nuxt-nightly@4.0.0-29191554.69345c32(@parcel/watcher@2.5.1)(@types/node@24.0.10)(@vue/compiler-sfc@3.5.17)(db0@0.3.2)(eslint@9.30.1(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.44.2)(terser@5.43.1)(typescript@5.8.3)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue-tsc@3.0.1(typescript@5.8.3))(yaml@2.8.0): dependencies: - '@nuxt/cli': '@nuxt/cli-nightly@3.26.0-20250702-114812-ac2d954(magicast@0.3.5)' + '@nuxt/cli': '@nuxt/cli-nightly@3.26.2-20250715-082518-d2c200e(magicast@0.3.5)' '@nuxt/devalue': 2.0.2 '@nuxt/devtools': 2.6.2(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3)) '@nuxt/kit': '@nuxt/kit-nightly@4.0.0-29191554.69345c32(magicast@0.3.5)' @@ -13850,6 +13888,8 @@ snapshots: process@0.11.10: {} + promise-polyfill@8.3.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 diff --git a/src/runtime/components/ScriptPaypalButtons.vue b/src/runtime/components/ScriptPaypalButtons.vue new file mode 100644 index 00000000..a086cf1c --- /dev/null +++ b/src/runtime/components/ScriptPaypalButtons.vue @@ -0,0 +1,188 @@ + + + diff --git a/src/runtime/components/ScriptPaypalMarks.vue b/src/runtime/components/ScriptPaypalMarks.vue new file mode 100644 index 00000000..a5d229bc --- /dev/null +++ b/src/runtime/components/ScriptPaypalMarks.vue @@ -0,0 +1,105 @@ + + + diff --git a/src/runtime/components/ScriptPaypalMessages.vue b/src/runtime/components/ScriptPaypalMessages.vue new file mode 100644 index 00000000..4cbfebdc --- /dev/null +++ b/src/runtime/components/ScriptPaypalMessages.vue @@ -0,0 +1,139 @@ + + + diff --git a/src/runtime/registry/paypal.ts b/src/runtime/registry/paypal.ts new file mode 100644 index 00000000..7bafee8b --- /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' + +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) +}