Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ jobs:
CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID }}
CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }}
CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }}
# Flip to 'true' once the sandbox OAuth clients are provisioned for the
# merchant-specific subdomain, and the suite will run against it instead of
# the shared hosts. See TestDomainConfiguration.
CHECKOUT_TEST_USE_SUBDOMAIN: 'false'
run: ./gradlew build test --fail-fast
4 changes: 4 additions & 0 deletions .github/workflows/build-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ jobs:
CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID }}
CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }}
CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }}
# Flip to 'true' once the sandbox OAuth clients are provisioned for the
# merchant-specific subdomain, and the suite will run against it instead of
# the shared hosts. See TestDomainConfiguration.
CHECKOUT_TEST_USE_SUBDOMAIN: 'false'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew build
4 changes: 4 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ jobs:
CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID }}
CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET }}
CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }}
# Flip to 'true' once the sandbox OAuth clients are provisioned for the
# merchant-specific subdomain, and the suite will run against it instead of
# the shared hosts. See TestDomainConfiguration.
CHECKOUT_TEST_USE_SUBDOMAIN: 'false'
run: ./gradlew build test --fail-fast jar
- id: publish
env:
Expand Down
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ If you don't have your own API keys, you can sign up for a test account [here](h

**PLEASE NEVER SHARE OR PUBLISH YOUR CHECKOUT CREDENTIALS.**

### Subdomain value

Requests must be made through your merchant-specific subdomain (MSSD): the first 8 characters of your client ID (excluding `cli_`). For example, if your client ID is `cli_vkuhvk4vjn2edkps7dfsq6emqm`, your subdomain is `vkuhvk4v`. When `environmentSubdomain` is set the SDK sends requests to `https://vkuhvk4v.api.checkout.com`. See [Base URLs](https://api-reference.checkout.com/#section/Base-URLs) and [API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints) for further details, and for where to find your unique client ID.

### Default

Default keys client instantiation can be done as follows:
Expand All @@ -99,7 +103,7 @@ public static void main(String[] args) {
.publicKey("public_key") // optional, only required for operations related with tokens
.secretKey("secret_key")
.environment(Environment.PRODUCTION) // required
.environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
.environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID
.executor() // optional for a custom Executor Service
.build();

Expand All @@ -125,7 +129,7 @@ final CheckoutApi checkoutApi = CheckoutSdk.builder()
//.clientCredentials(new URI("https://access.sandbox.checkout.com/connect/token"), "client_id", "client_secret")
.scopes(OAuthScope.GATEWAY, OAuthScope.VAULT, OAuthScope.FX)
.environment(Environment.PRODUCTION) // required
.environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
.environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID
.executor() // optional for a custom Executor Service
.build();

Expand All @@ -149,7 +153,7 @@ public static void main(String[] args) {
.publicKey("public_key") // optional, only required for operations related with tokens
.secretKey("secret_key")
.environment(Environment.PRODUCTION) // required
.environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
.environmentSubdomain("subdomain") // optional for the Previous platform, Merchant-specific DNS name
.executor() // optional for a custom Executor Service
.build();

Expand Down Expand Up @@ -399,7 +403,7 @@ final CheckoutApi checkoutApi = CheckoutSdk.builder()
.staticKeys()
.secretKey("secret_key")
.environment(Environment.PRODUCTION) // required
.environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
.environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID
.httpClientBuilder(customHttpClient) // optional for a custom HttpClient
.build();
```
Expand Down Expand Up @@ -656,6 +660,35 @@ final CheckoutApi checkoutApi = CheckoutSdk.builder()
- All resilience patterns are optional - configure only what you need
- Rate limiter helps respect API rate limits and prevent overwhelming the service

## Legacy domain (emergency use only)

> :warning: **Only use if merchant specific sub domains are causing issues.** Connecting through your merchant-specific subdomain (see [Subdomain value](#subdomain-value)) is the supported way of using the Checkout.com API, and non-subdomain usage will be deprecated.

If, in exceptional circumstances, you cannot use your merchant-specific subdomain, you can explicitly opt out by calling `useLegacyDomain()` instead of `environmentSubdomain(...)`:

```java
final CheckoutApi checkoutApi = CheckoutSdk.builder()
.staticKeys()
.secretKey("secret_key")
.environment(Environment.SANDBOX)
.useLegacyDomain() // deprecated, emergency fallback only
.build();
```

This routes requests to `api.checkout.com` (or `api.sandbox.checkout.com`) and `access.checkout.com` (or `access.sandbox.checkout.com`). The method is annotated `@Deprecated` and produces a compile-time warning. Exactly one of `environmentSubdomain(...)` or `useLegacyDomain()` must be set: the SDK throws a `CheckoutArgumentException` if both, or neither, are set. The Previous (ABC) platform predates merchant-specific subdomains and is exempt from this requirement.

## Running the tests against your subdomain

The test suite builds every client through `TestDomainConfiguration`, which has two modes. By default it uses the shared hosts, because the sandbox OAuth clients are not provisioned for merchant-specific subdomains and the token request would come back `invalid_client`. To run against a subdomain instead:

```bash
export CHECKOUT_MERCHANT_SUBDOMAIN="your_subdomain"
export CHECKOUT_TEST_USE_SUBDOMAIN=true
./gradlew test
```

The switch is separate from `CHECKOUT_MERCHANT_SUBDOMAIN` on purpose: CI already exports that secret, so provisioning is what should flip the behaviour, not the presence of a value. Once sandbox is provisioned like production, set `CHECKOUT_TEST_USE_SUBDOMAIN: 'true'` in the workflows and CI exercises the subdomain path end to end.

## Code of Conduct

Please refer to [Code of Conduct](CODE_OF_CONDUCT.md)
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group=com.checkout
version=7.15.0
version=8.0.0

project_name=Checkout SDK Java
project_description=Checkout SDK for Java https://checkout.com
Expand Down
42 changes: 39 additions & 3 deletions src/main/java/com/checkout/AbstractCheckoutSdkBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public abstract class AbstractCheckoutSdkBuilder<T extends CheckoutApiClient> {

protected HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
private IEnvironment environment;
private EnvironmentSubdomain environmentSubdomain;
private String subdomain;
private boolean useLegacyDomain;
private Executor executor = ForkJoinPool.commonPool();
private TransportConfiguration transportConfiguration;
private Boolean recordTelemetry = true;
Expand All @@ -25,7 +26,23 @@ public AbstractCheckoutSdkBuilder<T> environmentSubdomain(final String subdomain
if (subdomain == null) {
throw new CheckoutArgumentException("subdomain must be specified");
}
this.environmentSubdomain = new EnvironmentSubdomain(this.environment, subdomain);
this.subdomain = subdomain;
return this;
}

/**
* Opts out of the merchant-specific subdomain, sending every request to the shared
* hosts instead ({@code api.checkout.com} and {@code access.checkout.com}, or their
* sandbox equivalents).
*
* @deprecated this is an emergency fallback for the rare case where the
* merchant-specific subdomain cannot be used, and will be removed in a future release.
* Call {@link #environmentSubdomain(String)} instead.
* See <a href="https://api-reference.checkout.com/#section/Base-URLs">Base URLs</a>.
*/
@Deprecated
public AbstractCheckoutSdkBuilder<T> useLegacyDomain() {
this.useLegacyDomain = true;
return this;
}

Expand All @@ -49,7 +66,16 @@ protected IEnvironment getEnvironment() {
}

protected EnvironmentSubdomain getEnvironmentSubdomain() {
return environmentSubdomain;
return subdomain != null ? new EnvironmentSubdomain(environment, subdomain) : null;
}

/**
* Whether this builder requires the merchant-specific subdomain to be configured.
* The Previous (ABC) platform predates merchant-specific subdomains, so it overrides
* this to {@code false}.
*/
protected boolean requiresEnvironmentSubdomain() {
return true;
}

public AbstractCheckoutSdkBuilder<T> recordTelemetry(final Boolean recordTelemetry) {
Expand All @@ -73,13 +99,23 @@ protected CheckoutConfiguration getCheckoutConfiguration() {
if (environment == null) {
throw new CheckoutArgumentException("environment must be specified");
}
validateEnvironmentSettings();
final SdkCredentials sdkCredentials = getSdkCredentials();
if (transportConfiguration == null) {
transportConfiguration = new DefaultTransportConfiguration();
}
return buildCheckoutConfiguration(sdkCredentials);
}

private void validateEnvironmentSettings() {
if (subdomain != null && useLegacyDomain) {
throw new CheckoutArgumentException("environmentSubdomain and useLegacyDomain cannot both be set - provide only your merchant-specific subdomain");
}
if (subdomain == null && !useLegacyDomain && requiresEnvironmentSubdomain()) {
throw new CheckoutArgumentException("environmentSubdomain is required - provide your merchant-specific subdomain (the first 8 characters of your client ID, see https://api-reference.checkout.com/#section/Base-URLs), or call useLegacyDomain() to opt out only if merchant specific sub domains are causing issues");
}
}

private CheckoutConfiguration buildCheckoutConfiguration(final SdkCredentials sdkCredentials) {
return new DefaultCheckoutConfiguration(sdkCredentials, getEnvironment(), getEnvironmentSubdomain(), httpClientBuilder, executor, transportConfiguration, recordTelemetry, synchronous, resilience4jConfiguration);
}
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/checkout/CheckoutPreviousSdkBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ public static class CheckoutStaticKeysSdkBuilder extends AbstractCheckoutSdkBuil
private String publicKey;
private String secretKey;

// The Previous (ABC) platform predates merchant-specific subdomains, so it is exempt
// from the mandatory environmentSubdomain/useLegacyDomain configuration.
@Override
protected boolean requiresEnvironmentSubdomain() {
return false;
}

public CheckoutStaticKeysSdkBuilder publicKey(final String publicKey) {
this.publicKey = publicKey;
return this;
Expand Down
35 changes: 14 additions & 21 deletions src/main/java/com/checkout/EnvironmentSubdomain.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,29 @@ public URI getOAuthAuthorizationApi() {
}

/**
* Applies subdomain transformation to any given URI.
* If the subdomain is valid (alphanumeric pattern), prepends it to the host.
* Otherwise, returns the original URI unchanged.
* Applies subdomain transformation to any given URI, prepending the subdomain to the host.
*
* @param originalUrl the original URI to transform
* @param subdomain the subdomain to prepend
* @return the transformed URI with subdomain, or original URI if subdomain is invalid
* @return the transformed URI with subdomain
* @throws CheckoutArgumentException if the subdomain is not a valid merchant-specific subdomain
*/
private static URI createUrlWithSubdomain(URI originalUrl, String subdomain) {
URI newEnvironment = null;
Pattern pattern = Pattern.compile("^(?:pl-)?[a-z0-9]+$");
Matcher matcher = subdomain == null ? null : pattern.matcher(subdomain);
if (matcher == null || !matcher.matches()) {
throw new CheckoutArgumentException("invalid environment subdomain - provide your merchant-specific subdomain, the first 8 characters of your client ID (see https://api-reference.checkout.com/#section/Base-URLs)");
}

String host = originalUrl.getHost();
String scheme = originalUrl.getScheme();
int port = originalUrl.getPort();
String newHost = subdomain + "." + host;
try {
newEnvironment = new URI(originalUrl.toString());
return new URI(scheme, null, newHost, port, originalUrl.getPath(), originalUrl.getQuery(), originalUrl.getFragment());
} catch (final URISyntaxException e) {
throw new CheckoutException(e);
}

Pattern pattern = Pattern.compile("^(?:pl-)?[a-z0-9]+$");
Matcher matcher = pattern.matcher(subdomain);
if (matcher.matches()) {
String host = originalUrl.getHost();
String scheme = originalUrl.getScheme();
int port = originalUrl.getPort();
String newHost = subdomain + "." + host;
try {
newEnvironment = new URI(scheme, null, newHost, port, originalUrl.getPath(), originalUrl.getQuery(), originalUrl.getFragment());
} catch (final URISyntaxException e) {
throw new CheckoutException(e);
}
}
return newEnvironment;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ void shouldCreateCheckoutApiWithSynchronousMode() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.synchronous(true)
.build();

Expand All @@ -35,6 +36,7 @@ void shouldCreateCheckoutApiWithResilience4jConfiguration() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.resilience4jConfiguration(resilience4jConfig)
.build();

Expand All @@ -49,6 +51,7 @@ void shouldCreateCheckoutApiWithSynchronousAndResilience4j() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.synchronous(true)
.resilience4jConfiguration(resilience4jConfig)
.build();
Expand All @@ -63,6 +66,7 @@ void shouldCreateCheckoutApiWithoutNewParameters() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.build();

assertNotNull(checkoutApi);
Expand Down Expand Up @@ -90,6 +94,7 @@ void shouldCreateCheckoutApiWithCustomResilience4jConfiguration() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.synchronous(true)
.resilience4jConfiguration(resilience4jConfig)
.build();
Expand Down
Loading
Loading