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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ account [here](https://www.checkout.com/get-test-account).

**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 `with_environment_subdomain` 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 @@ -79,6 +83,7 @@ api = CheckoutSdk.builder
.with_secret_key('secret_key')
.with_public_key('public_key') # optional, only required for operations related with tokens
.with_environment(CheckoutSdk::Environment.sandbox)
.with_environment_subdomain('subdomain') # required, the first 8 characters of your client ID
.build
```

Expand All @@ -93,6 +98,7 @@ api = CheckoutSdk.builder
.with_client_credentials("client_id", "client_secret")
.with_scopes([CheckoutSdk::OAuthScopes::VAULT, CheckoutSdk::OAuthScopes::GATEWAY]) # array of scopes
.with_environment(CheckoutSdk::Environment.sandbox)
.with_environment_subdomain('subdomain') # required, the first 8 characters of your client ID
.build
```

Expand All @@ -107,6 +113,7 @@ api = CheckoutSdk.builder
.with_secret_key('secret_key')
.with_public_key('public_key') # optional, only required for operations related with tokens
.with_environment(CheckoutSdk::Environment.sandbox)
.with_environment_subdomain('subdomain') # optional for the Previous platform
.build
```

Expand Down Expand Up @@ -267,6 +274,23 @@ If you absolutely need to skip the pre-commit hooks:
OVERCOMMIT_DISABLE=1 git commit -m "your message"
```

## 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 `with_legacy_domain` instead of `with_environment_subdomain`:

```ruby
api = CheckoutSdk.builder
.static_keys
.with_secret_key('secret_key')
.with_environment(CheckoutSdk::Environment.sandbox)
.with_legacy_domain # 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 prints a deprecation warning. Exactly one of `with_environment_subdomain` or `with_legacy_domain` must be set: the SDK raises a `CheckoutSdk::CheckoutArgumentException` if both, or neither, are. The Previous (ABC) platform predates merchant-specific subdomains and is exempt from this requirement.

## Code of Conduct

Please refer to [Code of Conduct](CODE_OF_CONDUCT.md)
Expand Down
55 changes: 51 additions & 4 deletions lib/checkout_sdk/abstract_checkout_sdk_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ class AbstractCheckoutSdkBuilder
# @return [Faraday::Connection]
# @!attribute multipart_http_client
# @return [Faraday::Connection]
# @!attribute environment_subdomain
# @return [EnvironmentSubdomain, nil]
attr_accessor :environment, :http_client, :multipart_http_client, :logger, :environment_subdomain
# @!attribute subdomain
# @return [String, nil]
attr_accessor :environment, :http_client, :multipart_http_client, :logger, :subdomain

# @param [Environment] environment
def with_environment(environment)
Expand All @@ -37,10 +37,39 @@ def with_logger(logger)

# @param [String, nil] subdomain
def with_environment_subdomain(subdomain)
@environment_subdomain = EnvironmentSubdomain.new(@environment, subdomain)
@subdomain = subdomain
self
end

# Opts out of the merchant-specific subdomain, sending every request to the shared hosts
# instead (api.checkout.com and 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
# {#with_environment_subdomain} instead.
# See https://api-reference.checkout.com/#section/Base-URLs
def with_legacy_domain
warn '[DEPRECATION] with_legacy_domain is deprecated and will be removed in a future ' \
'release. It is intended only as an emergency fallback when the merchant-specific ' \
'subdomain cannot be used. Call with_environment_subdomain instead. ' \
'See https://api-reference.checkout.com/#section/Base-URLs'
@use_legacy_domain = true
self
end

# @return [EnvironmentSubdomain, nil]
def environment_subdomain
return nil if subdomain.nil?

EnvironmentSubdomain.new(environment, subdomain)
end

# Whether this builder requires the merchant-specific subdomain to be configured. The
# Previous (ABC) platform predates merchant-specific subdomains, so it overrides this.
def requires_environment_subdomain?
true
end

def build
with_environment(Environment.sandbox) if environment.nil?
if http_client.nil?
Expand All @@ -58,6 +87,24 @@ def build
end
end
@logger = SimpleLogger.new.logger if @logger.nil?
validate_environment_settings
end

private

def validate_environment_settings
if !subdomain.nil? && @use_legacy_domain
raise CheckoutArgumentException,
'with_environment_subdomain and with_legacy_domain cannot both be set - provide ' \
'only your merchant-specific subdomain'
end
return unless subdomain.nil? && !@use_legacy_domain && requires_environment_subdomain?

raise CheckoutArgumentException,
'environment subdomain 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 with_legacy_domain ' \
'to opt out only if merchant specific sub domains are causing issues'
end
end
end
45 changes: 22 additions & 23 deletions lib/checkout_sdk/environment_subdomain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,35 @@ def initialize(environment, subdomain)

private

# 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 original_url [String] The original URL to transform.
# @param subdomain [String] The subdomain to prepend to the host.
# @return [String] The transformed URL with subdomain, or original URL if subdomain is invalid.
# @return [String] The transformed URL with subdomain.
# @raise [CheckoutArgumentException] If the subdomain is not a valid merchant-specific
# subdomain.
def create_url_with_subdomain(original_url, subdomain)
new_environment = original_url

if subdomain =~ /^(?:pl-)?[a-z0-9]+$/
url_parts = URI.parse(original_url)
new_host = "#{subdomain}.#{url_parts.host}"

port = url_parts.scheme == 'https' && url_parts.port == 443 ? nil : url_parts.port
unless subdomain =~ /^(?:pl-)?[a-z0-9]+$/
raise 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)'
end

new_url_parts = URI::Generic.build(
scheme: url_parts.scheme,
userinfo: url_parts.userinfo,
host: new_host,
port: port,
path: url_parts.path,
query: url_parts.query,
fragment: url_parts.fragment
)
url_parts = URI.parse(original_url)
new_host = "#{subdomain}.#{url_parts.host}"

new_environment = new_url_parts.to_s
end
port = url_parts.scheme == 'https' && url_parts.port == 443 ? nil : url_parts.port

new_environment
URI::Generic.build(
scheme: url_parts.scheme,
userinfo: url_parts.userinfo,
host: new_host,
port: port,
path: url_parts.path,
query: url_parts.query,
fragment: url_parts.fragment
).to_s
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ class CheckoutPreviousStaticKeysSdkBuilder < StaticKeysBuilder
PUBLIC_KEY_PATTERN = '^pk_(test_)?(\\w{8})-(\\w{4})-(\\w{4})-(\\w{4})-(\\w{12})$'
private_constant :SECRET_KEY_PATTERN, :PUBLIC_KEY_PATTERN

# The Previous (ABC) platform predates merchant-specific subdomains, so it is exempt from
# the mandatory with_environment_subdomain/with_legacy_domain configuration.
def requires_environment_subdomain?
false
end

def build
@secret_key_pattern = SECRET_KEY_PATTERN
@public_key_pattern = PUBLIC_KEY_PATTERN
Expand Down
17 changes: 14 additions & 3 deletions spec/checkout_sdk/accounts/accounts_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,14 @@ def payout_schedules_checkout_api
.oauth
.with_client_credentials(
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_ID', nil),
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET', nil))
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET', nil)
)
.with_scopes([CheckoutSdk::OAuthScopes::MARKETPLACE])
.with_environment(CheckoutSdk::Environment.sandbox)
# The sandbox OAuth clients are not provisioned for the merchant-specific subdomain,
# so the token request would come back invalid_client. Opting out explicitly until
# they are.
.with_legacy_domain
.build
end

Expand All @@ -378,9 +383,12 @@ def accounts_checkout_api
.oauth
.with_client_credentials(
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_ID', nil),
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_SECRET', nil))
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_SECRET', nil)
)
.with_scopes([CheckoutSdk::OAuthScopes::ACCOUNTS, CheckoutSdk::OAuthScopes::FILES])
.with_environment(CheckoutSdk::Environment.sandbox)
# See payout_schedules_checkout_api above for why the legacy domain is used here.
.with_legacy_domain
.build
end

Expand All @@ -389,8 +397,11 @@ def files_checkout_api
.oauth
.with_client_credentials(
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_ID', nil),
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_SECRET', nil))
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_SECRET', nil)
)
.with_scopes([CheckoutSdk::OAuthScopes::FILES])
.with_environment(CheckoutSdk::Environment.sandbox)
# See payout_schedules_checkout_api above for why the legacy domain is used here.
.with_legacy_domain
.build
end
38 changes: 6 additions & 32 deletions spec/checkout_sdk/configuration/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,38 +72,12 @@ class FakeLogger
end
end

[
['', 'https://api.sandbox.checkout.com/'],
[' ', 'https://api.sandbox.checkout.com/'],
[' ', 'https://api.sandbox.checkout.com/'],
[' - ', 'https://api.sandbox.checkout.com/'],
['a b', 'https://api.sandbox.checkout.com/'],
['ab bc1', 'https://api.sandbox.checkout.com/'],
['foo-', 'https://api.sandbox.checkout.com/'],
['-foo', 'https://api.sandbox.checkout.com/'],
['FOO', 'https://api.sandbox.checkout.com/'],
['Foo-Bar', 'https://api.sandbox.checkout.com/'],
['test-123', 'https://api.sandbox.checkout.com/'],
['foo-bar', 'https://api.sandbox.checkout.com/'],
['pl-', 'https://api.sandbox.checkout.com/']
].each do |subdomain, expected_url|
it "should create configuration with bad subdomain #{subdomain}" do
environment_subdomain = CheckoutSdk::EnvironmentSubdomain.new(CheckoutSdk::Environment.sandbox, subdomain)

configuration = CheckoutSdk::CheckoutConfiguration.new(
@credentials,
CheckoutSdk::Environment.sandbox,
@http_client,
@multipart_http_client,
@logger,
environment_subdomain
)

expect(configuration.credentials).to eq(@credentials)
expect(configuration.environment.base_uri).to eq(CheckoutSdk::Environment.sandbox.base_uri)
expect(configuration.http_client).to eq(@http_client)
expect(configuration.environment_subdomain.base_uri).to eq(expected_url)
expect(configuration.environment_subdomain.authorization_uri).to eq('https://access.sandbox.checkout.com/connect/token')
['', ' ', ' ', ' - ', 'a b', 'ab bc1', 'foo-', '-foo', 'FOO', 'Foo-Bar', 'test-123', 'foo-bar',
'pl-'].each do |subdomain|
it "should fail with bad subdomain #{subdomain}" do
expect do
CheckoutSdk::EnvironmentSubdomain.new(CheckoutSdk::Environment.sandbox, subdomain)
end.to raise_error(CheckoutSdk::CheckoutArgumentException, /invalid environment subdomain/)
end
end

Expand Down
28 changes: 17 additions & 11 deletions spec/checkout_sdk/issuing/issuing_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@
module IssuingHelper
def get_issuing_api
if @issuing_api.nil?
@issuing_api = CheckoutSdk.builder
.oauth
.with_client_credentials(ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID', nil),
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET', nil))
.with_scopes([CheckoutSdk::OAuthScopes::VAULT,
CheckoutSdk::OAuthScopes::ISSUING_CLIENT,
CheckoutSdk::OAuthScopes::ISSUING_CARD_MGMT,
CheckoutSdk::OAuthScopes::ISSUING_CONTROLS_READ,
CheckoutSdk::OAuthScopes::ISSUING_CONTROLS_WRITE])
.with_environment(CheckoutSdk::Environment.sandbox)
.build
builder = CheckoutSdk.builder
.oauth
.with_client_credentials(
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID', nil),
ENV.fetch('CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET', nil)
)
.with_scopes([CheckoutSdk::OAuthScopes::VAULT,
CheckoutSdk::OAuthScopes::ISSUING_CLIENT,
CheckoutSdk::OAuthScopes::ISSUING_CARD_MGMT,
CheckoutSdk::OAuthScopes::ISSUING_CONTROLS_READ,
CheckoutSdk::OAuthScopes::ISSUING_CONTROLS_WRITE])
.with_environment(CheckoutSdk::Environment.sandbox)
# The sandbox OAuth clients are not provisioned for the
# merchant-specific subdomain, so the token request would come back
# invalid_client. Opting out explicitly until they are.
.with_legacy_domain
@issuing_api = builder.build
end
@issuing_api
end
Expand Down
Loading
Loading