Skip to main content

StripeClient

\Benecaster\Payment\StripeClient

Class Free

Wraps the Stripe PHP SDK and owns every Stripe credential Benecaster holds. Shared infrastructure rather than a private detail of any one feature — Listener Support donations, built-in membership subscriptions and buy-ups all resolve their credentials through it.

Two rules matter to anyone integrating, and both are easy to get wrong from the method names alone. Credentials resolve per show, then site-wide — a show may hold its own Stripe account, so anything that creates Stripe objects must resolve for the show it is acting on. And a site-wide keyset is a default, not a prerequisite — an install may run every show on its own account with no site-wide keys at all, which is a supported configuration rather than an unfinished setup.

Keys are stored per mode (test and live). The active mode resolves per show too — a show’s own mode when it has a complete keyset for one, the site-wide setting otherwise — so the site-wide toggle is a default a show inherits, not the answer for the install. Secret keys and webhook signing secrets are encrypted at rest. Publishable keys are stored in plain text because they are published to the browser anyway.

Constructor Dependencies

Type Description
\Benecaster\Payment\Crypto Encrypts and decrypts secret keys and webhook signing secrets at rest.

Methods

Method Visibility Since Description
sdk( ?int $show_id = null ): ?object Public SDK bound to the active mode's credentials for a show, falling back to site-wide. Returns null when nothing resolves.
sdk_for_mode( string $mode ): ?object Public SDK for a named mode using SITE-WIDE credentials only. Provisioners must not use this — see sdk_for_show_mode() and the notes below.
sdk_for_show_mode( ?int $show_id, string $mode ): ?object Public SDK for a named mode, resolving the show's own credentials first and site-wide second. The correct entry point for anything that creates Stripe objects.
is_test_mode( ?int $show_id = null ): bool Public True when the active keyset for the requested scope is the test set. $show_id of null means the install default, not "the current mode" — there is no such thing once shows can differ. Any caller holding a show must pass it; a bare call at a show-bearing call site returns the site answer to a question that was about one show, which is how a live Price ID reaches an SDK built from a test secret. Read from the stored mode, never inferred from a key prefix — inference would silently flip mode on a mistyped key.
get_mode( ?int $show_id = null ): string Public The active keyset mode for a show: its own when it has one, the install default otherwise. This is the supported way to answer "what mode is this show in".
get_show_mode( int $show_id ): ?string Public A show's own mode, or null when it follows the install default. A stored mode is honoured only while the show carries a complete keyset for it. A show on the site account has no mode to have — there the key is the mode — and a show that had its own keys, set a mode, then lost those keys falls back to the install default rather than serving a phantom mode against somebody else's account.
site_is_test_mode(): bool Public The install default, ignoring every per-show override. The supported reader when the default itself is the question — use it in place of reading benecaster_stripe_test_mode directly.
save_show_mode( int $show_id, ?string $mode ): true|WP_Error Public Persists a show's own mode; null clears it and returns the show to the install default. Deliberately does NOT reject a mode the show has no keyset for. The rule lives on the read side, in get_show_mode(), for two reasons: a settings screen saving a keyset and a mode in one request cannot then fail on field ordering, and a show that loses its keys later is corrected without needing a write. Rejects only a value that is neither test nor live.
set_test_mode( bool $enabled ): void Public Sets the install-wide default mode. Per-show modes win over it — this does not change what a show with its own keyset is doing.
configured_modes( ?int $show_id = null ): array Public Which modes have usable credentials. Pass the show — asked site-wide, a show overriding only live would still report test as configured and be provisioned into the wrong account.
configured_show_modes( int $show_id ): array Public Which modes this show OVERRIDES, ignoring site-wide credentials. Answers a different question from configured_modes(): does this show have its own account, rather than what would it charge with.
is_configured_anywhere(): bool Public True when usable credentials exist site-wide OR on any single show. The correct gate for "can this install take money at all". Memoised per request; a $wpdb meta lookup rather than get_posts(), because callers can run before init.
shows_with_own_keys(): array Public Show IDs holding their own Stripe credentials in either mode.
save_keyset( string $mode, string $public_key, string $secret_key ): true|WP_Error Public Persist a site-wide keyset. Prefix-validated; a mismatch returns WP_Error and writes nothing. Empty values clear the keyset.
save_show_keys( int $show_id, string $mode, string $public_key, string $secret_key ): true|WP_Error Public Persist one mode's per-show override, leaving the other mode and the show's webhook secret untouched. Clearing both keys for a mode removes that mode's override rather than storing empty strings.
save_show_webhook_secret( int $show_id, string $webhook_secret ): void Public Persist the show's webhook signing secret. Mode-independent. Empty clears the override and the show falls back to the site-wide secret.
save_legacy_keys( string $public_key, string $secret_key, string $webhook_secret = '' ): void Public Writes the RETIRED mode-less slot. Product code must not call this, and it is not a way to configure Stripe — it survives for one purpose: constructing a legacy-shaped install in an upgrade test.

Constants

Name Value Description
SHOW_META_KEYS _benecaster_stripe_keys Show meta holding per-show credential overrides. The payload is split by mode — { test: { public_key, secret_key }, live: { … }, webhook_secret }. Treat it as private storage and go through this class rather than reading the meta directly; a payload still in the flat shape is read as no override at all, which is deliberate.
SHOW_MODE_FIELD mode Key holding a show's own mode inside the _benecaster_stripe_keys payload. Absent when the show follows the install default. Go through this class rather than reading the meta.
MODE_TEST test Test-mode identifier used by every mode-taking method.
MODE_LIVE live Live-mode identifier used by every mode-taking method.
OPTION_TEST_MODE benecaster_stripe_test_mode The install's default mode — not the active mode. Reading this option does not tell you whether any given show is taking real money. A show with its own Stripe keyset can set its own mode and overrides it either way, so a bare get_option() was a correct answer before per-show mode shipped and is a wrong one after — the worst kind of stale code, because it still runs and returns a plausible boolean. Ask get_mode( $show_id ) or is_test_mode( $show_id ) instead; site_is_test_mode() is the supported reader when the install default genuinely is the question. Its previous description here justified the option as site-wide by design, on the grounds that a per-show equivalent would multiply "am I taking real money?" by the number of shows — that reasoning was reversed when per-show mode shipped.

Notes

Provisioners must resolve per show. Use sdk_for_show_mode(), never sdk_for_mode(). The subscription and customer paths already resolve per show, so a provisioner that resolves site-wide creates the Product and Price in one Stripe account while the Subscription referencing them is created in another — and Stripe rejects a cross-account price ID. That is a broken subscribe path, not a cosmetic inconsistency.

is_test_mode() and get_mode() gained an optional ?int $show_id, and a no-argument call still returns exactly what it always did — which is the problem. The name and the old behaviour are identical, so a reader will assume nothing changed. The no-argument answer is about the INSTALL DEFAULT, and since per-show mode shipped that is not the same question as "what is this show doing". Nothing errors; the value is just quietly about something else.

Call-site rule: anything holding a show must pass it. A bare call at a show-bearing call site answers the site question in place of the show one — which is how a live Price ID reaches an SDK built from a test secret. Benecaster's own account dashboard is a deliberate bare caller, because it spans every show a subscriber belongs to and has no single show to ask about.

get_show_mode() returns null for two different situations, deliberately. A show that inherits the install default returns null — and so does a show carrying a stored mode it is not entitled to, because it has no complete keyset for that mode. Both mean the same thing to a caller ("this show follows the site"), which is why they are not distinguished: a show on the site account has no mode to have, since there the key is the mode. It also disposes of the stale-override case for free — a show that had its own keys, set a mode, then had those keys removed falls back to the install default instead of serving a phantom mode against somebody else's Stripe account.

``php // What the show is ACTUALLY transacting in. This is almost always the // question, and it never returns null — it falls back to the default. $mode = $stripe->get_mode( $show_id ); // 'test' | 'live' // Whether the show has a mode of its OWN. $own = $stripe->get_show_mode( $show_id ); // 'test' | 'live' | null if ( null === $own ) { // Do NOT read this as "the podcaster never chose a mode". They may // have chosen one and since had the keys for it removed — same null. // If what you need is whether the show has its own Stripe account, // that is a different method and the only one that answers it: $has_own_account = [] !== $stripe->configured_show_modes( $show_id ); } ``

Read that example as the rule it encodes: get_show_mode() is not a settings reader. It answers "does this show override, and with what" — a question about effect, not about what somebody typed. Nothing in this class will tell you a stored-but-unhonoured mode is there, and that is deliberate: the only code that legitimately cares is the settings screen, which asks configured_show_modes() to decide whether the control is offered at all.

The mode-less legacy key pair (benecaster_stripe_public_key / _secret_key) is RETIRED — retired rather than deprecated, because no code path returns it any more. It was a read fallback for an empty active keyset, and having no mode of its own it was a cross-mode fallback: a live pair left behind was served to a site sitting in test mode, i.e. real cards charged from a test environment. The upgrade routine drains it into the keyset its prefixes identify.

The options are deliberately NOT deleted. They stay in wp_options, inert, so an operator can still recover an old key. Their presence does not mean Stripe is configured — anything treating it that way, or telling a reader to clear them, is wrong. Ask is_configured_anywhere() or configured_modes().

Key prefixes are validated on write and the write is refused on mismatch. sk_test_ for test, sk_live_ for live, and nothing is stored when the check fails. This is what prevents a live key landing in the test slot and charging real cards from a test environment. Per-show and site-wide saves share the validator so the two cannot drift apart.

A _benecaster_stripe_keys payload whose mode cannot be determined from its prefix is left in place rather than guessed at, so the show falls back to the site-wide account instead of risking a live charge from a test environment.