credentials(); } /** * Cloud credentials, or null before registration. * * @return array{shop_id: string, key_id: string, secret: string}|null */ public function credentials(): ?array { $cloud = $this->get( 'cloud' ); if ( ! is_array( $cloud ) || empty( $cloud['shop_id'] ) || empty( $cloud['key_id'] ) || empty( $cloud['secret'] ) ) { return null; } return array( 'shop_id' => (string) $cloud['shop_id'], 'key_id' => (string) $cloud['key_id'], 'secret' => (string) $cloud['secret'], ); } /** * Market chosen at registration. */ public function market(): ?string { $cloud = $this->get( 'cloud' ); return is_array( $cloud ) && isset( $cloud['market'] ) ? (string) $cloud['market'] : null; } /** * ID of the WooCommerce API key created for Storebridge Cloud. */ public function woo_api_key_id(): ?int { $cloud = $this->get( 'cloud' ); return is_array( $cloud ) && isset( $cloud['woo_api_key_id'] ) ? (int) $cloud['woo_api_key_id'] : null; } /** * Store the credentials returned by POST /v1/shops/register. * * @param string $shop_id Cloud shop id. * @param string $key_id Public key id. * @param string $secret Signing secret. * @param string $market Market code. * @param int $woo_api_key_id WooCommerce API key id. * @param string $status Initial shop status. */ public function save_registration( string $shop_id, string $key_id, string $secret, string $market, int $woo_api_key_id, string $status ): void { $this->set( 'cloud', compact( 'shop_id', 'key_id', 'secret', 'market', 'woo_api_key_id' ) ); $this->save_connection( array( 'status' => $status, 'tiktok' => null, 'reason' => null, ) ); } /** * Last known connection state (from a status call or a push from the cloud). * * @return array{status: string, tiktok: array|null, reason: array|null, updated_at: string}|null */ public function connection(): ?array { $connection = $this->get( 'connection' ); if ( ! is_array( $connection ) || ! isset( $connection['status'], $connection['updated_at'] ) ) { return null; } return array( 'status' => (string) $connection['status'], 'tiktok' => isset( $connection['tiktok'] ) && is_array( $connection['tiktok'] ) ? $connection['tiktok'] : null, 'reason' => isset( $connection['reason'] ) && is_array( $connection['reason'] ) ? $connection['reason'] : null, 'updated_at' => (string) $connection['updated_at'], ); } /** * Store a connection snapshot. * * Pushes from the cloud carry the cloud's `occurred_at`; a push older than the last push is an * out-of-order delivery and is ignored. Snapshots taken locally (registration, status calls) are * always stored, so a difference between the WordPress and cloud clocks never drops an update. * * @param array{status: string, tiktok: array|null, reason: array|null} $connection Snapshot. * @param string|null $pushed_at `occurred_at` of a cloud push; null for local snapshots. * @return bool False when the push was older than the last one. */ public function save_connection( array $connection, ?string $pushed_at = null ): bool { $stored = $this->get( 'connection' ); $last_push = is_array( $stored ) && isset( $stored['pushed_at'] ) ? (string) $stored['pushed_at'] : null; if ( null !== $pushed_at && null !== $last_push && strtotime( $last_push ) > strtotime( $pushed_at ) ) { return false; } $this->set( 'connection', array_merge( $connection, array( 'updated_at' => gmdate( 'c' ), 'pushed_at' => $pushed_at ?? $last_push, ) ) ); return true; } /** * Plan and limits from the last status call; Free until the cloud says otherwise. * * @return array{plan: string, limits: array, orders_waiting: int} */ public function plan(): array { $plan = $this->get( 'plan' ); return array( 'plan' => is_array( $plan ) && isset( $plan['plan'] ) ? (string) $plan['plan'] : 'free', 'limits' => is_array( $plan ) && isset( $plan['limits'] ) && is_array( $plan['limits'] ) ? $plan['limits'] : array( 'max_products' => 25, 'auto_order_import' => false, 'max_shops' => 1, ), 'orders_waiting' => is_array( $plan ) && isset( $plan['orders_waiting'] ) ? (int) $plan['orders_waiting'] : 0, ); } /** * Store the plan from a status call. * * @param array{plan: string, limits: array, orders_waiting: int} $plan Plan snapshot. */ public function save_plan( array $plan ): void { $this->set( 'plan', $plan ); } /** * Current onboarding wizard step: one of ONBOARDING_STEPS, or 'done'. * * Stores that connected TikTok Shop before the wizard existed are marked done, so existing * merchants are not sent through it. */ public function onboarding_step(): string { $step = $this->get( 'onboarding_step' ); if ( is_string( $step ) && ( 'done' === $step || in_array( $step, self::ONBOARDING_STEPS, true ) ) ) { return $step; } $connection = $this->connection(); if ( null !== $connection && in_array( $connection['status'], array( 'connected', 'reauth_required' ), true ) ) { $this->save_onboarding_step( 'done' ); return 'done'; } return self::ONBOARDING_STEPS[0]; } /** * Store the onboarding step. * * @param string $step One of ONBOARDING_STEPS, or 'done'. */ public function save_onboarding_step( string $step ): void { $this->set( 'onboarding_step', $step ); if ( 'done' === $step ) { $this->set( 'onboarding_completed_at', gmdate( 'c' ) ); $this->set( 'onboarding_skipped', false ); } } /** * Whether the merchant finished the wizard once (it can be reopened to edit details). */ public function onboarding_completed(): bool { return null !== $this->get( 'onboarding_completed_at' ); } /** * Whether the merchant chose to skip the wizard for now. */ public function onboarding_skipped(): bool { return true === $this->get( 'onboarding_skipped' ); } /** * Skip or resume the wizard. * * @param bool $skipped Whether the wizard is skipped. */ public function set_onboarding_skipped( bool $skipped ): void { $this->set( 'onboarding_skipped', $skipped ); } /** * Last known account state (contract AccountState) from Storebridge Cloud, or null. * * @return array|null */ public function account(): ?array { $account = $this->get( 'account' ); return is_array( $account ) && isset( $account['current_terms_version'] ) ? $account : null; } /** * Store the account state returned by the cloud. * * @param array $account Contract AccountState. */ public function save_account( array $account ): void { $this->set( 'account', $account ); } /** * Remove everything (used by uninstall and tests). */ public function forget(): void { delete_option( self::OPTION ); } /** * Read a top-level key. * * @param string $key Key. * @return mixed */ private function get( string $key ) { return $this->load()[ $key ] ?? null; } /** * Write a top-level key. * * @param string $key Key. * @param mixed $value Value. */ private function set( string $key, $value ): void { $data = $this->load(); $data[ $key ] = $value; update_option( self::OPTION, $data, false ); } /** * Read the option (cached by WordPress' object cache). * * @return array */ private function load(): array { $value = get_option( self::OPTION, array() ); return is_array( $value ) ? $value : array(); } }