diff --git a/inc/Integrations/Google/Sheets/GoogleSheetsDataSource.php b/inc/Integrations/Google/Sheets/GoogleSheetsDataSource.php index a521dbb2..a7b8ef51 100644 --- a/inc/Integrations/Google/Sheets/GoogleSheetsDataSource.php +++ b/inc/Integrations/Google/Sheets/GoogleSheetsDataSource.php @@ -12,7 +12,7 @@ class GoogleSheetsDataSource extends HttpDataSource { protected const SERVICE_SCHEMA = [ 'type' => 'object', - 'properties' => [ + 'properties' => [ 'credentials' => [ 'type' => 'object', 'properties' => [ diff --git a/inc/Integrations/SalesforceB2C/Auth/SalesforceB2CAuth.php b/inc/Integrations/SalesforceB2C/Auth/SalesforceB2CAuth.php new file mode 100644 index 00000000..458d985d --- /dev/null +++ b/inc/Integrations/SalesforceB2C/Auth/SalesforceB2CAuth.php @@ -0,0 +1,240 @@ + [ + 'grant_type' => 'client_credentials', + 'channel_id' => 'RefArch', + ], + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Authorization' => 'Basic ' . $client_credentials, + ], + ]); + + if ( is_wp_error( $client_auth_response ) ) { + return new WP_Error( + 'salesforce_b2c_auth_error_client_credentials', + __( 'Failed to retrieve access token from client credentials', 'remote-data-blocks' ) + ); + } + + $response_code = wp_remote_retrieve_response_code( $client_auth_response ); + $response_body = wp_remote_retrieve_body( $client_auth_response ); + $response_data = json_decode( $response_body, true ); + + if ( 400 === $response_code ) { + return new WP_Error( + 'salesforce_b2c_auth_error_client_credentials', + /* translators: %s: Technical error message from API containing failure reason */ + sprintf( __( 'Failed to retrieve access token from client credentials: "%s"', 'remote-data-blocks' ), $response_data['message'] ) + ); + } + + $access_token = $response_data['access_token']; + $access_token_expires_in = $response_data['expires_in']; + self::save_access_token( $access_token, $access_token_expires_in, $organization_id, $client_id ); + + $refresh_token = $response_data['refresh_token']; + $refresh_token_expires_in = $response_data['refresh_token_expires_in']; + self::save_refresh_token( $refresh_token, $refresh_token_expires_in, $organization_id, $client_id ); + + return $access_token; + } + + // Access token request using refresh token + + public static function get_token_using_refresh_token( + string $refresh_token, + string $client_id, + string $client_secret, + string $endpoint, + string $organization_id, + ): ?string { + $client_auth_url = sprintf( '%s/shopper/auth/v1/organizations/%s/oauth2/token', $endpoint, $organization_id ); + + // Even though we're using a refresh token, authentication is still required to receive a new secret + $client_credentials = base64_encode( sprintf( '%s:%s', $client_id, $client_secret ) ); + + $client_auth_response = wp_remote_post($client_auth_url, [ + 'body' => [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $refresh_token, + 'channel_id' => 'RefArch', + ], + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Authorization' => 'Basic ' . $client_credentials, + ], + ]); + + if ( is_wp_error( $client_auth_response ) ) { + return null; + } + + $response_code = wp_remote_retrieve_response_code( $client_auth_response ); + $response_body = wp_remote_retrieve_body( $client_auth_response ); + $response_data = json_decode( $response_body, true ); + + if ( 400 === $response_code ) { + return null; + } + + $access_token = $response_data['access_token']; + $access_token_expires_in = $response_data['expires_in']; + self::save_access_token( $access_token, $access_token_expires_in, $organization_id, $client_id ); + + // No need to save the refresh token, as it stays the same until we perform a top-level authentication + + return $access_token; + } + + // Access token cache management + + private static function save_access_token( string $access_token, int $expires_in, string $organization_id, string $client_id ): void { + // Expires 10 seconds early as a buffer for request time and drift + $access_token_expires_in = $expires_in - 10; + + $access_token_data = [ + 'token' => $access_token, + 'expires_at' => time() + $access_token_expires_in, + ]; + + $access_token_cache_key = self::get_access_token_key( $organization_id, $client_id ); + + wp_cache_set( + $access_token_cache_key, + $access_token_data, + 'oauth-tokens', + // phpcs:ignore WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined -- 'expires_in' defaults to 30 minutes for access tokens. + $access_token_expires_in, + ); + } + + private static function get_saved_access_token( string $organization_id, string $client_id ): ?string { + $access_token_cache_key = self::get_access_token_key( $organization_id, $client_id ); + + $saved_access_token = wp_cache_get( $access_token_cache_key, 'oauth-tokens' ); + + if ( false === $saved_access_token ) { + return null; + } + + $access_token = $saved_access_token['token']; + $expires_at = $saved_access_token['expires_at']; + + // Ensure the token is still valid + if ( time() >= $expires_at ) { + return null; + } + + return $access_token; + } + + private static function get_access_token_key( string $organization_id, string $client_id ): string { + $cache_key_suffix = hash( 'sha256', sprintf( '%s-%s', $organization_id, $client_id ) ); + return sprintf( 'salesforce_b2c_access_token_%s', $cache_key_suffix ); + } + + // Refresh token cache management + + private static function save_refresh_token( string $refresh_token, int $expires_in, string $organization_id, string $client_id ): void { + // Expires 10 seconds early as a buffer for request time and drift + $refresh_token_expires_in = $expires_in - 10; + + $refresh_token_data = [ + 'token' => $refresh_token, + 'expires_at' => time() + $refresh_token_expires_in, + ]; + + $refresh_token_cache_key = self::get_refresh_token_cache_key( $organization_id, $client_id ); + + wp_cache_set( + $refresh_token_cache_key, + $refresh_token_data, + 'oauth-tokens', + // phpcs:ignore WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined -- 'expires_in' defaults to 30 days for refresh tokens. + $refresh_token_expires_in, + ); + } + + private static function get_saved_refresh_token( string $organization_id, string $client_id ): ?string { + $refresh_token_cache_key = self::get_refresh_token_cache_key( $organization_id, $client_id ); + + $saved_refresh_token = wp_cache_get( $refresh_token_cache_key, 'oauth-tokens' ); + + if ( false === $saved_refresh_token ) { + return null; + } + + $refresh_token = $saved_refresh_token['token']; + $expires_at = $saved_refresh_token['expires_at']; + + // Ensure the token is still valid + if ( time() >= $expires_at ) { + return null; + } + + return $refresh_token; + } + + private static function get_refresh_token_cache_key( string $organization_id, string $client_id ): string { + $cache_key_suffix = hash( 'sha256', sprintf( '%s-%s', $organization_id, $client_id ) ); + return sprintf( 'salesforce_b2c_refresh_token_%s', $cache_key_suffix ); + } +} diff --git a/inc/Integrations/SalesforceB2C/Queries/SalesforceB2CGetProductQuery.php b/inc/Integrations/SalesforceB2C/Queries/SalesforceB2CGetProductQuery.php new file mode 100644 index 00000000..22e95118 --- /dev/null +++ b/inc/Integrations/SalesforceB2C/Queries/SalesforceB2CGetProductQuery.php @@ -0,0 +1,96 @@ + [ + 'name' => 'Product ID', + 'overrides' => [ + [ + 'target' => 'utm_content', + 'type' => 'query_var', + ], + ], + 'type' => 'id', + ], + ]; + } + + public function get_output_schema(): array { + return [ + 'is_collection' => false, + 'mappings' => [ + 'id' => [ + 'name' => 'Product ID', + 'path' => '$.id', + 'type' => 'id', + ], + 'name' => [ + 'name' => 'Name', + 'path' => '$.name', + 'type' => 'string', + ], + 'longDescription' => [ + 'name' => 'Long Description', + 'path' => '$.longDescription', + 'type' => 'string', + ], + 'price' => [ + 'name' => 'Price', + 'path' => '$.price', + 'type' => 'string', + ], + 'image_url' => [ + 'name' => 'Image URL', + 'path' => '$.imageGroups[0].images[0].link', + 'type' => 'image_url', + ], + 'image_alt_text' => [ + 'name' => 'Image Alt Text', + 'path' => '$.imageGroups[0].images[0].alt', + 'type' => 'image_alt', + ], + ], + ]; + } + + public function get_request_headers( array $input_variables ): array { + $data_source_config = $this->get_data_source()->to_array(); + $data_source_endpoint = $this->get_data_source()->get_endpoint(); + + $access_token = SalesforceB2CAuth::generate_token( + $data_source_endpoint, + $data_source_config['organization_id'], + $data_source_config['client_id'], + $data_source_config['client_secret'] + ); + + $headers = [ + 'Content-Type' => 'application/json', + ]; + + if ( is_wp_error( $access_token ) ) { + return $headers; + } + + $headers['Authorization'] = sprintf( 'Bearer %s', $access_token ); + return $headers; + } + + public function get_endpoint( array $input_variables ): string { + $data_source_endpoint = $this->get_data_source()->get_endpoint(); + $data_source_config = $this->get_data_source()->to_array(); + + return sprintf( '%s/product/shopper-products/v1/organizations/%s/products/%s?siteId=RefArchGlobal', $data_source_endpoint, $data_source_config['organization_id'], $input_variables['product_id'] ); + } + + public function get_query_name(): string { + return $this->config['query_name'] ?? 'Get item'; + } +} diff --git a/inc/Integrations/SalesforceB2C/Queries/SalesforceB2CSearchProductsQuery.php b/inc/Integrations/SalesforceB2C/Queries/SalesforceB2CSearchProductsQuery.php new file mode 100644 index 00000000..64ac9cb7 --- /dev/null +++ b/inc/Integrations/SalesforceB2C/Queries/SalesforceB2CSearchProductsQuery.php @@ -0,0 +1,84 @@ + [ + 'type' => 'string', + ], + ]; + } + + public function get_output_schema(): array { + return [ + 'root_path' => '$.hits[*]', + 'is_collection' => true, + 'mappings' => [ + 'product_id' => [ + 'name' => 'Product ID', + 'path' => '$.productId', + 'type' => 'id', + ], + 'name' => [ + 'name' => 'Product name', + 'path' => '$.productName', + 'type' => 'string', + ], + 'price' => [ + 'name' => 'Item price', + 'path' => '$.price', + 'type' => 'price', + ], + 'image_url' => [ + 'name' => 'Item image URL', + 'path' => '$.image.link', + 'type' => 'image_url', + ], + ], + ]; + } + + public function get_endpoint( array $input_variables ): string { + $data_source_endpoint = $this->get_data_source()->get_endpoint(); + $data_source_config = $this->get_data_source()->to_array(); + + return sprintf( + '%s/search/shopper-search/v1/organizations/%s/product-search?siteId=RefArchGlobal&q=%s', + $data_source_endpoint, + $data_source_config['organization_id'], + urlencode( $input_variables['search_terms'] ) + ); + } + + public function get_request_headers( array $input_variables ): array { + $data_source_config = $this->get_data_source()->to_array(); + $data_source_endpoint = $this->get_data_source()->get_endpoint(); + + $access_token = SalesforceB2CAuth::generate_token( + $data_source_endpoint, + $data_source_config['organization_id'], + $data_source_config['client_id'], + $data_source_config['client_secret'] + ); + + $headers = [ + 'Content-Type' => 'application/json', + ]; + + if ( is_wp_error( $access_token ) ) { + return $headers; + } + + $headers['Authorization'] = sprintf( 'Bearer %s', $access_token ); + return $headers; + } + + public function get_query_name(): string { + return 'Search products'; + } +} diff --git a/inc/Integrations/SalesforceB2C/SalesforceB2CDataSource.php b/inc/Integrations/SalesforceB2C/SalesforceB2CDataSource.php new file mode 100644 index 00000000..c07bbef8 --- /dev/null +++ b/inc/Integrations/SalesforceB2C/SalesforceB2CDataSource.php @@ -0,0 +1,70 @@ + 'object', + 'properties' => [ + 'service' => [ + 'type' => 'string', + 'const' => REMOTE_DATA_BLOCKS_SALESFORCE_B2C_SERVICE, + ], + 'service_schema_version' => [ + 'type' => 'integer', + 'const' => self::SERVICE_SCHEMA_VERSION, + ], + 'shortcode' => [ 'type' => 'string' ], + 'organization_id' => [ 'type' => 'string' ], + 'client_id' => [ 'type' => 'string' ], + 'client_secret' => [ 'type' => 'string' ], + ], + ]; + + public function get_display_name(): string { + return 'Salesforce B2C (' . $this->config['slug'] . ')'; + } + + public function get_endpoint(): string { + return sprintf( 'https://%s.api.commercecloud.salesforce.com', $this->config['shortcode'] ); + } + + public function get_request_headers(): array { + return [ + 'Content-Type' => 'application/json', + ]; + } + + public function get_image_url(): string { + return plugins_url( './assets/salesforce_commerce_cloud_logo.png', __FILE__ ); + } + + public static function create( string $shortcode, string $organization_id, string $client_id, string $client_secret ): self { + return parent::from_array([ + 'service' => REMOTE_DATA_BLOCKS_SALESFORCE_B2C_SERVICE, + 'shortcode' => $shortcode, + 'organization_id' => $organization_id, + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'slug' => sanitize_title( sprintf( '%s/%s', $shortcode, $organization_id ) ), + ]); + } + + public function to_ui_display(): array { + return [ + 'slug' => $this->get_slug(), + 'service' => REMOTE_DATA_BLOCKS_SALESFORCE_B2C_SERVICE, + 'store_name' => $this->config['store_name'], + 'uuid' => $this->config['uuid'] ?? null, + ]; + } +} diff --git a/inc/Integrations/SalesforceB2C/SalesforceB2CIntegration.php b/inc/Integrations/SalesforceB2C/SalesforceB2CIntegration.php new file mode 100644 index 00000000..a355ec6c --- /dev/null +++ b/inc/Integrations/SalesforceB2C/SalesforceB2CIntegration.php @@ -0,0 +1,30 @@ +get_display_name(); + register_remote_data_block( $block_name, $salesforce_get_product_query ); + register_remote_data_search_query( $block_name, $salesforce_search_products_query ); + + LoggerManager::instance()->info( 'Registered Salesforce B2C block', [ 'block_name' => $block_name ] ); + } +} diff --git a/inc/Integrations/SalesforceB2C/assets/salesforce_commerce_cloud_logo.png b/inc/Integrations/SalesforceB2C/assets/salesforce_commerce_cloud_logo.png new file mode 100644 index 00000000..e075c917 Binary files /dev/null and b/inc/Integrations/SalesforceB2C/assets/salesforce_commerce_cloud_logo.png differ diff --git a/inc/Integrations/constants.php b/inc/Integrations/constants.php index ba0e5222..3ea6d947 100644 --- a/inc/Integrations/constants.php +++ b/inc/Integrations/constants.php @@ -7,6 +7,7 @@ define( 'REMOTE_DATA_BLOCKS_GITHUB_SERVICE', 'github' ); define( 'REMOTE_DATA_BLOCKS_GOOGLE_SHEETS_SERVICE', 'google-sheets' ); define( 'REMOTE_DATA_BLOCKS_SHOPIFY_SERVICE', 'shopify' ); +define( 'REMOTE_DATA_BLOCKS_SALESFORCE_B2C_SERVICE', 'salesforce-b2c' ); define( 'REMOTE_DATA_BLOCKS_MOCK_SERVICE', 'mock' ); define( 'REMOTE_DATA_BLOCKS__SERVICES', [ @@ -14,6 +15,7 @@ REMOTE_DATA_BLOCKS_GENERIC_HTTP_SERVICE, REMOTE_DATA_BLOCKS_GITHUB_SERVICE, REMOTE_DATA_BLOCKS_GOOGLE_SHEETS_SERVICE, + REMOTE_DATA_BLOCKS_SALESFORCE_B2C_SERVICE, REMOTE_DATA_BLOCKS_SHOPIFY_SERVICE, ] ); @@ -22,6 +24,7 @@ REMOTE_DATA_BLOCKS_GENERIC_HTTP_SERVICE => \RemoteDataBlocks\Integrations\GenericHttp\GenericHttpDataSource::class, REMOTE_DATA_BLOCKS_GITHUB_SERVICE => \RemoteDataBlocks\Integrations\GitHub\GitHubDataSource::class, REMOTE_DATA_BLOCKS_GOOGLE_SHEETS_SERVICE => \RemoteDataBlocks\Integrations\Google\Sheets\GoogleSheetsDataSource::class, + REMOTE_DATA_BLOCKS_SALESFORCE_B2C_SERVICE => \RemoteDataBlocks\Integrations\SalesforceB2C\SalesforceB2CDataSource::class, REMOTE_DATA_BLOCKS_SHOPIFY_SERVICE => \RemoteDataBlocks\Integrations\Shopify\ShopifyDataSource::class, REMOTE_DATA_BLOCKS_MOCK_SERVICE => \RemoteDataBlocks\Tests\Mocks\MockDataSource::class, ]; diff --git a/remote-data-blocks.php b/remote-data-blocks.php index 63340437..daac97e3 100644 --- a/remote-data-blocks.php +++ b/remote-data-blocks.php @@ -46,6 +46,7 @@ // Integrations Integrations\Airtable\AirtableIntegration::init(); Integrations\Shopify\ShopifyIntegration::init(); +Integrations\SalesforceB2C\SalesforceB2CIntegration::init(); Integrations\VipBlockDataApi\VipBlockDataApi::init(); // REST endpoints diff --git a/src/data-sources/DataSourceSettings.tsx b/src/data-sources/DataSourceSettings.tsx index 641f0cf8..5d435e11 100644 --- a/src/data-sources/DataSourceSettings.tsx +++ b/src/data-sources/DataSourceSettings.tsx @@ -4,6 +4,7 @@ import { AirtableSettings } from '@/data-sources/airtable/AirtableSettings'; import { GoogleSheetsSettings } from '@/data-sources/google-sheets/GoogleSheetsSettings'; import { useDataSources } from '@/data-sources/hooks/useDataSources'; import { HttpSettings } from '@/data-sources/http/HttpSettings'; +import { SalesforceB2CSettings } from '@/data-sources/salesforce-b2c/SalesforceB2CSettings'; import { ShopifySettings } from '@/data-sources/shopify/ShopifySettings'; import { useSettingsContext } from '@/settings/hooks/useSettingsNav'; @@ -18,22 +19,21 @@ const DataSourceEditSettings = ( { uuid }: DataSourceEditSettings ) => { if ( loadingDataSources ) { return <>{ __( 'Loading data source...', 'remote-data-blocks' ) }; } + const dataSource = dataSources.find( source => source.uuid === uuid ); + if ( ! dataSource ) { return <>{ __( 'Data Source not found.', 'remote-data-blocks' ) }; - } - if ( 'airtable' === dataSource.service ) { + } else if ( 'airtable' === dataSource.service ) { return ; - } - if ( 'shopify' === dataSource.service ) { - return ; - } - - if ( 'google-sheets' === dataSource.service ) { - return ; - } - if ( 'generic-http' === dataSource.service ) { + } else if ( 'generic-http' === dataSource.service ) { return ; + } else if ( 'google-sheets' === dataSource.service ) { + return ; + } else if ( 'shopify' === dataSource.service ) { + return ; + } else if ( 'salesforce-b2c' === dataSource.service ) { + return ; } return <>{ __( 'Service not (yet) supported.', 'remote-data-blocks' ) }; @@ -46,15 +46,14 @@ const DataSourceSettings = () => { if ( 'add' === mode ) { if ( 'airtable' === service ) { return ; - } - if ( 'shopify' === service ) { - return ; - } - if ( 'google-sheets' === service ) { - return ; - } - if ( 'generic-http' === service ) { + } else if ( 'generic-http' === service ) { return ; + } else if ( 'google-sheets' === service ) { + return ; + } else if ( 'shopify' === service ) { + return ; + } else if ( 'salesforce-b2c' === service ) { + return ; } return <>{ __( 'Service not (yet) supported.', 'remote-data-blocks' ) }; } diff --git a/src/data-sources/components/AddDataSourceDropdown.tsx b/src/data-sources/components/AddDataSourceDropdown.tsx index afac4109..bda7885e 100644 --- a/src/data-sources/components/AddDataSourceDropdown.tsx +++ b/src/data-sources/components/AddDataSourceDropdown.tsx @@ -7,6 +7,7 @@ import { useSettingsContext } from '@/settings/hooks/useSettingsNav'; import AirtableIcon from '@/settings/icons/AirtableIcon'; import GoogleSheetsIcon from '@/settings/icons/GoogleSheetsIcon'; import HttpIcon from '@/settings/icons/HttpIcon'; +import SalesforceCommerceB2CIcon from '@/settings/icons/SalesforceCommerceB2CIcon'; import ShopifyIcon from '@/settings/icons/ShopifyIcon'; import '../DataSourceList.scss'; @@ -50,6 +51,11 @@ export const AddDataSourceDropdown = () => { label: SUPPORTED_SERVICES_LABELS.shopify, value: 'shopify', }, + { + icon: SalesforceCommerceB2CIcon, + label: SUPPORTED_SERVICES_LABELS[ 'salesforce-b2c' ], + value: 'salesforce-b2c', + }, { icon: HttpIcon, label: SUPPORTED_SERVICES_LABELS[ 'generic-http' ], diff --git a/src/data-sources/constants.ts b/src/data-sources/constants.ts index 43da866e..3d0bd439 100644 --- a/src/data-sources/constants.ts +++ b/src/data-sources/constants.ts @@ -4,18 +4,20 @@ import { SelectOption } from '@/types/input'; export const SUPPORTED_SERVICES = [ 'airtable', - 'shopify', - 'google-sheets', - 'generic-http', 'example-api', + 'generic-http', + 'google-sheets', + 'salesforce-b2c', + 'shopify', ] as const; export const SUPPORTED_SERVICES_LABELS: Record< ( typeof SUPPORTED_SERVICES )[ number ], string > = { airtable: __( 'Airtable', 'remote-data-blocks' ), - shopify: __( 'Shopify', 'remote-data-blocks' ), - 'google-sheets': __( 'Google Sheets', 'remote-data-blocks' ), - 'generic-http': __( 'HTTP', 'remote-data-blocks' ), 'example-api': __( 'Conference Events Example API', 'remote-data-blocks' ), + 'generic-http': __( 'HTTP', 'remote-data-blocks' ), + 'google-sheets': __( 'Google Sheets', 'remote-data-blocks' ), + 'salesforce-b2c': __( 'Salesforce Commerce B2C', 'remote-data-blocks' ), + shopify: __( 'Shopify', 'remote-data-blocks' ), } as const; export const OPTIONS_PAGE_SLUG = 'remote-data-blocks-settings'; export const REST_BASE = '/remote-data-blocks/v1'; diff --git a/src/data-sources/salesforce-b2c/SalesforceB2CSettings.tsx b/src/data-sources/salesforce-b2c/SalesforceB2CSettings.tsx new file mode 100644 index 00000000..3c164bdc --- /dev/null +++ b/src/data-sources/salesforce-b2c/SalesforceB2CSettings.tsx @@ -0,0 +1,156 @@ +import { TextControl } from '@wordpress/components'; +import { useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +import { DataSourceForm } from '../components/DataSourceForm'; +import { DataSourceFormActions } from '@/data-sources/components/DataSourceFormActions'; +import PasswordInputControl from '@/data-sources/components/PasswordInputControl'; +import { SlugInput } from '@/data-sources/components/SlugInput'; +import { useDataSources } from '@/data-sources/hooks/useDataSources'; +import { SettingsComponentProps, SalesforceB2CConfig } from '@/data-sources/types'; +import { useForm } from '@/hooks/useForm'; +import { useSettingsContext } from '@/settings/hooks/useSettingsNav'; + +export type SalesforceB2CFormState = Omit< SalesforceB2CConfig, 'service' | 'uuid' >; + +const initialState: SalesforceB2CFormState = { + shortcode: '', + organization_id: '', + client_id: '', + client_secret: '', + slug: '', +}; + +const getInitialStateFromConfig = ( config?: SalesforceB2CConfig ): SalesforceB2CFormState => { + if ( ! config ) { + return initialState; + } + return { + shortcode: config.shortcode, + organization_id: config.organization_id, + client_id: config.client_id, + client_secret: config.client_secret, + slug: config.slug, + }; +}; + +export const SalesforceB2CSettings = ( { + mode, + uuid: uuidFromProps, + config, +}: SettingsComponentProps< SalesforceB2CConfig > ) => { + const { goToMainScreen } = useSettingsContext(); + const { updateDataSource, addDataSource } = useDataSources( false ); + + const { state, handleOnChange } = useForm< SalesforceB2CFormState >( { + initialValues: getInitialStateFromConfig( config ), + } ); + + const shouldAllowSubmit = useMemo( () => { + return ( + state.slug && + state.shortcode && + state.organization_id && + state.client_id && + state.client_secret + ); + }, [ state.slug, state.shortcode, state.organization_id, state.client_id, state.client_secret ] ); + + const onSaveClick = async () => { + const salesforceConfig: SalesforceB2CConfig = { + uuid: uuidFromProps ?? '', + service: 'salesforce-b2c', + shortcode: state.shortcode, + organization_id: state.organization_id, + client_id: state.client_id, + client_secret: state.client_secret, + slug: state.slug, + }; + + if ( mode === 'add' ) { + await addDataSource( salesforceConfig ); + } else { + await updateDataSource( salesforceConfig ); + } + + goToMainScreen(); + }; + + return ( + +
+ { + handleOnChange( 'slug', slug ?? '' ); + } } + uuid={ uuidFromProps } + /> +
+ +
+ { + handleOnChange( 'shortcode', shortCode ?? '' ); + } } + value={ state.shortcode } + help={ __( 'The region-specific merchant identifier. Example: 0dnz6ope' ) } + autoComplete="off" + __next40pxDefaultSize + /> +
+ +
+ { + handleOnChange( 'organization_id', shortCode ?? '' ); + } } + value={ state.organization_id } + help={ __( 'The organization ID. Example: f_ecom_mirl_012' ) } + autoComplete="off" + __next40pxDefaultSize + /> +
+ +
+ { + handleOnChange( 'client_id', shortCode ?? '' ); + } } + value={ state.client_id } + help={ __( 'Example: bc2991f1-eec8-4976-8774-935cbbe84f18' ) } + autoComplete="off" + __next40pxDefaultSize + /> +
+ +
+ { + handleOnChange( 'client_secret', shortCode ?? '' ); + } } + value={ state.client_secret } + /> +
+ + +
+ ); +}; diff --git a/src/data-sources/types.ts b/src/data-sources/types.ts index 2c7bba12..b8fe8029 100644 --- a/src/data-sources/types.ts +++ b/src/data-sources/types.ts @@ -44,12 +44,6 @@ export interface AirtableConfig extends BaseDataSourceConfig { tables: AirtableTableConfig[]; } -export interface ShopifyConfig extends BaseDataSourceConfig { - service: 'shopify'; - access_token: string; - store_name: string; -} - export interface GoogleSheetsConfig extends BaseDataSourceConfig { service: 'google-sheets'; credentials: GoogleServiceAccountKey; @@ -63,7 +57,26 @@ export interface HttpConfig extends BaseDataSourceConfig { auth: HttpAuth; } -export type DataSourceConfig = AirtableConfig | ShopifyConfig | GoogleSheetsConfig | HttpConfig; +export interface SalesforceB2CConfig extends BaseDataSourceConfig { + service: 'salesforce-b2c'; + shortcode: string; + organization_id: string; + client_id: string; + client_secret: string; +} + +export interface ShopifyConfig extends BaseDataSourceConfig { + service: 'shopify'; + access_token: string; + store_name: string; +} + +export type DataSourceConfig = + | AirtableConfig + | GoogleSheetsConfig + | HttpConfig + | SalesforceB2CConfig + | ShopifyConfig; export type SettingsComponentProps< T extends BaseDataSourceConfig > = { mode: 'add' | 'edit'; diff --git a/src/settings/icons/SalesforceCommerceB2CIcon.tsx b/src/settings/icons/SalesforceCommerceB2CIcon.tsx new file mode 100644 index 00000000..3fafb357 --- /dev/null +++ b/src/settings/icons/SalesforceCommerceB2CIcon.tsx @@ -0,0 +1,109 @@ +import { SVG, Path, G } from '@wordpress/primitives'; + +const SalesforceCommerceB2CIcon = ( + + + + + + + + + + + + + + + + + + + + + + + +); + +export default SalesforceCommerceB2CIcon; diff --git a/tsconfig.json b/tsconfig.json index a5717259..9ab0212f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "skipLibCheck": true, "strict": true, "target": "es6", - "typeRoots": [ "./types", "./node_modules/@types" ] + "typeRoots": [ "./types", "./node_modules/@types" ], + "sourceMap": true }, "include": [ "./types/*.d.ts", "./example/**/*", "./src/**/*", "./tests/**/*", "*.config.ts" ] }