Skip to content

Commit fc38ff0

Browse files
committed
Merge branch 'dev/develop' into dev/PCP-6554-v2-apple-pay-visible-under-paypal-gateway-when-vaulting-subscription-is-in-cart
2 parents 81a919a + d63383e commit fc38ff0

15 files changed

Lines changed: 934 additions & 243 deletions

modules/ppcp-api-client/services.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
3939
use WooCommerce\PayPalCommerce\ApiClient\Factory\ContactPreferenceFactory;
4040
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExchangeRateFactory;
41+
use WooCommerce\PayPalCommerce\ApiClient\Factory\PartnersEndpointFactory;
42+
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayPalBearerFactory;
4143
use WooCommerce\PayPalCommerce\ApiClient\Factory\FraudProcessorResponseFactory;
4244
use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory;
4345
use WooCommerce\PayPalCommerce\ApiClient\Factory\MoneyFactory;
@@ -172,6 +174,31 @@
172174
$container->get( 'api.partners-seller-status-cache' )
173175
);
174176
},
177+
/**
178+
* Factory for connection-aware bearers authenticated with explicit credentials.
179+
*/
180+
'api.factory.paypal-bearer' => static function ( ContainerInterface $container ): PayPalBearerFactory {
181+
return new PayPalBearerFactory(
182+
$container->get( 'api.token-rate-limiter' ),
183+
$container->get( 'woocommerce.logger.woocommerce' )
184+
);
185+
},
186+
/**
187+
* Factory for PartnersEndpoint instances.
188+
* Creates a fresh bearer on every call to reflect the current connection and
189+
* environment state.
190+
*/
191+
'api.factory.partners-endpoint' => static function ( ContainerInterface $container ): PartnersEndpointFactory {
192+
return new PartnersEndpointFactory(
193+
$container->get( 'api.env.paypal-host' ),
194+
$container->get( 'api.env.partner-id' ),
195+
$container->get( 'api.factory.sellerstatus' ),
196+
$container->get( 'api.helper.failure-registry' ),
197+
$container->get( 'api.partners-seller-status-cache' ),
198+
$container->get( 'api.factory.paypal-bearer' ),
199+
$container->get( 'woocommerce.logger.woocommerce' )
200+
);
201+
},
175202
'api.partners-seller-status-cache' => static function ( ContainerInterface $container ): Cache {
176203
return new Cache( 'ppcp-seller-status-' );
177204
},
@@ -944,6 +971,18 @@ static function ( ContainerInterface $container ): PurchaseUnitSanitizer {
944971
$container->get( 'api.paypal-host-sandbox' )
945972
);
946973
},
974+
'api.env.partner-id' => static function ( ContainerInterface $container ): EnvironmentConfig {
975+
/**
976+
* Environment specific partner ID.
977+
*
978+
* @type EnvironmentConfig<string>
979+
*/
980+
return EnvironmentConfig::create(
981+
'string',
982+
$container->get( 'api.partner_merchant_id-production' ),
983+
$container->get( 'api.partner_merchant_id-sandbox' )
984+
);
985+
},
947986
'api.env.endpoint.login-seller' => static function ( ContainerInterface $container ): EnvironmentConfig {
948987
/**
949988
* Environment specific LoginSeller API instances.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
/**
3+
* Builds a PartnersEndpoint with a dedicated bearer for given credentials.
4+
*
5+
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
6+
*/
7+
8+
declare( strict_types = 1 );
9+
10+
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
11+
12+
use Psr\Log\LoggerInterface;
13+
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
14+
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
15+
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
16+
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
17+
18+
/**
19+
* Builds a PartnersEndpoint for an explicit set of merchant credentials, backed
20+
* by its own freshly minted bearer.
21+
*
22+
* The container's shared PartnersEndpoint binds its bearer, host and merchant ID
23+
* to the connection state that existed when it was first resolved during the
24+
* request. This factory takes the credentials as arguments instead, so a caller
25+
* can obtain a working endpoint for any credentials, regardless of the active
26+
* connection or when in the request it is called.
27+
*/
28+
class PartnersEndpointFactory {
29+
30+
/**
31+
* PayPal API host, per environment.
32+
*
33+
* @var EnvironmentConfig<string>
34+
*/
35+
private EnvironmentConfig $paypal_host;
36+
37+
/**
38+
* Partner merchant ID, per environment.
39+
*
40+
* @var EnvironmentConfig<string>
41+
*/
42+
private EnvironmentConfig $partner_id;
43+
44+
private SellerStatusFactory $seller_status_factory;
45+
46+
private FailureRegistry $failure_registry;
47+
48+
private Cache $cache;
49+
50+
private PayPalBearerFactory $bearer_factory;
51+
52+
private LoggerInterface $logger;
53+
54+
/**
55+
* @param EnvironmentConfig<string> $paypal_host
56+
* @param EnvironmentConfig<string> $partner_id
57+
* @param SellerStatusFactory $seller_status_factory
58+
* @param FailureRegistry $failure_registry
59+
* @param Cache $cache
60+
* @param PayPalBearerFactory $bearer_factory
61+
* @param LoggerInterface $logger
62+
*
63+
* phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
64+
*/
65+
public function __construct(
66+
EnvironmentConfig $paypal_host,
67+
EnvironmentConfig $partner_id,
68+
SellerStatusFactory $seller_status_factory,
69+
FailureRegistry $failure_registry,
70+
Cache $cache,
71+
PayPalBearerFactory $bearer_factory,
72+
LoggerInterface $logger
73+
) {
74+
$this->paypal_host = $paypal_host;
75+
$this->partner_id = $partner_id;
76+
$this->seller_status_factory = $seller_status_factory;
77+
$this->failure_registry = $failure_registry;
78+
$this->cache = $cache;
79+
$this->bearer_factory = $bearer_factory;
80+
$this->logger = $logger;
81+
}
82+
83+
/**
84+
* Builds a PartnersEndpoint for the given merchant credentials.
85+
*
86+
* @param bool $is_sandbox Whether the credentials are for the sandbox.
87+
* @param string $client_id The merchant client ID.
88+
* @param string $client_secret The merchant client secret.
89+
* @param string $merchant_id The merchant ID.
90+
*/
91+
public function create(
92+
bool $is_sandbox,
93+
string $client_id,
94+
string $client_secret,
95+
string $merchant_id
96+
): PartnersEndpoint {
97+
$host = (string) $this->paypal_host->get_value( $is_sandbox );
98+
99+
$bearer = $this->bearer_factory->create( $host, $client_id, $client_secret );
100+
101+
return new PartnersEndpoint(
102+
$host,
103+
$bearer,
104+
$this->logger,
105+
$this->seller_status_factory,
106+
(string) $this->partner_id->get_value( $is_sandbox ),
107+
$merchant_id,
108+
$this->failure_registry,
109+
$this->cache
110+
);
111+
}
112+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
/**
3+
* Builds the API bearer for a set of credentials.
4+
*
5+
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
6+
*/
7+
8+
declare( strict_types = 1 );
9+
10+
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
11+
12+
use Psr\Log\LoggerInterface;
13+
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
14+
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
15+
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
16+
use WooCommerce\PayPalCommerce\ApiClient\Authentication\TokenRateLimiter;
17+
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
18+
use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache;
19+
use WooCommerce\PayPalCommerce\Settings\Data\SettingsProvider;
20+
21+
/**
22+
* Produces the bearer that matches the supplied credentials.
23+
*
24+
* Credential presence is the connection signal: with both a client ID and secret
25+
* the factory builds an authenticated PayPalBearer, otherwise it returns a
26+
* login-only ConnectBearer (nothing is connected, so there is nothing to
27+
* authenticate with). This mirrors PayPalBearer itself, which cannot fetch a token
28+
* without credentials.
29+
*
30+
* By default the PayPalBearer gets an isolated in-memory token cache and static
31+
* credentials, scoping its token to the passed credentials so it never mixes with
32+
* another account's cached token. Pass a persistent cache and a settings provider
33+
* to reproduce the shared, connection-bound bearer instead; when the settings
34+
* provider resolves credentials dynamically, still pass the current client ID and
35+
* secret so the bearer-type decision stays correct.
36+
*/
37+
class PayPalBearerFactory {
38+
39+
private TokenRateLimiter $rate_limiter;
40+
41+
private LoggerInterface $logger;
42+
43+
public function __construct( TokenRateLimiter $rate_limiter, LoggerInterface $logger ) {
44+
$this->rate_limiter = $rate_limiter;
45+
$this->logger = $logger;
46+
}
47+
48+
/**
49+
* Builds the bearer for the given credentials.
50+
*
51+
* Returns an authenticated PayPalBearer when both credentials are present,
52+
* otherwise a login-only ConnectBearer.
53+
*
54+
* @param string $host The PayPal API host.
55+
* @param string $client_id The client ID.
56+
* @param string $client_secret The client secret.
57+
* @param ?Cache $cache Token cache; defaults to an isolated in-memory cache.
58+
* @param ?SettingsProvider $settings Resolves credentials dynamically when provided.
59+
*/
60+
public function create(
61+
string $host = '',
62+
string $client_id = '',
63+
string $client_secret = '',
64+
?Cache $cache = null,
65+
?SettingsProvider $settings = null
66+
): Bearer {
67+
if ( '' === $client_id || '' === $client_secret ) {
68+
return new ConnectBearer();
69+
}
70+
71+
return new PayPalBearer(
72+
$cache ?? new InMemoryCache(),
73+
$host,
74+
$client_id,
75+
$client_secret,
76+
$this->logger,
77+
$settings,
78+
$this->rate_limiter
79+
);
80+
}
81+
}

modules/ppcp-settings/services.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingNotices;
7272
use WooCommerce\PayPalCommerce\Settings\Service\OnboardingUrlManager;
7373
use WooCommerce\PayPalCommerce\Settings\Service\SellerTypeResolver;
74+
use WooCommerce\PayPalCommerce\Settings\Service\MerchantDataResolver;
7475
use WooCommerce\PayPalCommerce\Settings\Service\PaymentMethodsEligibilityService;
7576
use WooCommerce\PayPalCommerce\Settings\Service\ScriptDataHandler;
7677
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
@@ -88,7 +89,6 @@
8889
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
8990
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
9091
use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState;
91-
use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService;
9292
use WooCommerce\PayPalCommerce\WcGateway\Helper\MerchantDetails;
9393

9494
return array(
@@ -390,12 +390,7 @@
390390
$container->get( 'api.env.paypal-host' ),
391391
$container->get( 'api.env.endpoint.login-seller' ),
392392
$container->get( 'settings.connection-state' ),
393-
$container->get( 'settings.service.rest-service' ),
394-
$container->get( 'woocommerce.logger.woocommerce' )
395-
);
396-
},
397-
'settings.service.rest-service' => static function ( ContainerInterface $container ): InternalRestService {
398-
return new InternalRestService(
393+
$container->get( 'api.factory.paypal-bearer' ),
399394
$container->get( 'woocommerce.logger.woocommerce' )
400395
);
401396
},
@@ -463,6 +458,11 @@
463458
$c->get( 'ppcp-local-apms.payment-methods' ),
464459
),
465460
'settings.service.seller-type-resolver' => static fn(): SellerTypeResolver => new SellerTypeResolver(),
461+
'settings.service.merchant-data-resolver' => static fn( ContainerInterface $container ): MerchantDataResolver => new MerchantDataResolver(
462+
$container->get( 'settings.data.general' ),
463+
$container->get( 'api.factory.partners-endpoint' ),
464+
$container->get( 'woocommerce.logger.woocommerce' )
465+
),
466466
'settings.service.data-migration.general-settings' => static fn( ContainerInterface $c ): SettingsMigration => new SettingsMigration(
467467
(array) get_option( 'woocommerce-ppcp-settings', array() ),
468468
$c->get( 'settings.data.general' ),

modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,11 @@ public function __construct(
8888
string $seller_type = SellerTypeEnum::UNKNOWN
8989
) {
9090
$this->is_sandbox = $is_sandbox;
91-
$this->client_id = $client_id;
92-
$this->client_secret = $client_secret;
93-
$this->merchant_id = $merchant_id;
94-
$this->merchant_email = $merchant_email;
95-
$this->merchant_country = $merchant_country;
96-
$this->seller_type = $seller_type;
91+
$this->client_id = trim( $client_id );
92+
$this->client_secret = trim( $client_secret );
93+
$this->merchant_id = trim( $merchant_id );
94+
$this->merchant_email = trim( $merchant_email );
95+
$this->merchant_country = trim( $merchant_country );
96+
$this->seller_type = trim( $seller_type );
9797
}
9898
}

modules/ppcp-settings/src/Data/GeneralSettings.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ public function set_merchant_data( MerchantConnectionDTO $connection ): void {
165165
/**
166166
* Returns the full merchant connection DTO for the current connection.
167167
*
168+
* The `merchant_country` on the returned DTO is the raw value reported by
169+
* PayPal, and is empty when PayPal did not provide a country (e.g. the
170+
* post-connect enrichment failed). Unlike `self::get_merchant_country()`,
171+
* this getter does NOT fall back to the WooCommerce store country, so callers
172+
* can tell an unknown PayPal country apart from a known one.
173+
*
168174
* @return MerchantConnectionDTO All connection details.
169175
*/
170176
public function get_merchant_data(): MerchantConnectionDTO {
@@ -263,7 +269,11 @@ public function get_merchant_email(): string {
263269
}
264270

265271
/**
266-
* Gets the currently connected merchant's country.
272+
* Gets the merchant's country, falling back to the WooCommerce store country.
273+
*
274+
* This is the "safe" reader that always returns a usable country: when PayPal did not
275+
* report a merchant country, it falls back to the configured Woo store country. Use
276+
* it for country-based feature gating where a best-effort country is acceptable.
267277
*
268278
* @return string
269279
*/
@@ -322,6 +332,7 @@ public function reset_installation_path( string $reason ): bool {
322332
}
323333

324334
$this->data['wc_installation_path'] = '';
335+
325336
return true;
326337
}
327338

0 commit comments

Comments
 (0)