diff --git a/packages/js/ai/changelog/add-background-removal-feedback-39730 b/packages/js/ai/changelog/add-background-removal-feedback-39730
new file mode 100644
index 00000000000..33d2e80af0d
--- /dev/null
+++ b/packages/js/ai/changelog/add-background-removal-feedback-39730
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding feedback snackbar after image background removal
diff --git a/packages/js/ai/changelog/add-image-bkgrnd-removal-hook b/packages/js/ai/changelog/add-image-bkgrnd-removal-hook
new file mode 100644
index 00000000000..b33ced9b18b
--- /dev/null
+++ b/packages/js/ai/changelog/add-image-bkgrnd-removal-hook
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add useBackgroundRemoval hook for image background removal API requests.
diff --git a/packages/js/ai/changelog/add-remove-background-ui-40440 b/packages/js/ai/changelog/add-remove-background-ui-40440
new file mode 100644
index 00000000000..29524695a03
--- /dev/null
+++ b/packages/js/ai/changelog/add-remove-background-ui-40440
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Reworking error handling and return value for useBackgroundRemoval hook.
diff --git a/packages/js/ai/src/hooks/index.ts b/packages/js/ai/src/hooks/index.ts
index 41dea0bed94..1452bdff9f1 100644
--- a/packages/js/ai/src/hooks/index.ts
+++ b/packages/js/ai/src/hooks/index.ts
@@ -1 +1,2 @@
export * from './useCompletion';
+export * from './useBackgroundRemoval';
diff --git a/packages/js/ai/src/hooks/useBackgroundRemoval.test.ts b/packages/js/ai/src/hooks/useBackgroundRemoval.test.ts
new file mode 100644
index 00000000000..6d010e4b295
--- /dev/null
+++ b/packages/js/ai/src/hooks/useBackgroundRemoval.test.ts
@@ -0,0 +1,165 @@
+/**
+ * External dependencies
+ */
+import { renderHook, act } from '@testing-library/react-hooks/dom';
+import { waitFor } from '@testing-library/react';
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Internal dependencies
+ */
+import {
+ BackgroundRemovalParams,
+ useBackgroundRemoval,
+} from './useBackgroundRemoval';
+import { requestJetpackToken } from '../utils/requestJetpackToken';
+
+// Mocking the apiFetch function
+jest.mock( '@wordpress/api-fetch', () =>
+ jest.fn().mockResolvedValue( {
+ blob: () =>
+ Promise.resolve(
+ new Blob( [ new ArrayBuffer( 51200 ) ], {
+ type: 'image/jpeg',
+ } )
+ ),
+ } )
+);
+jest.mock( '../utils/requestJetpackToken' );
+const mockedRequestJetpackToken = requestJetpackToken as jest.MockedFunction<
+ typeof requestJetpackToken
+>;
+
+describe( 'useBackgroundRemoval hook', () => {
+ let mockRequestParams: BackgroundRemovalParams;
+
+ beforeEach( () => {
+ jest.resetAllMocks();
+ // Initialize with valid parameters (50kb image file).
+ const imageFile = new File( [ new ArrayBuffer( 51200 ) ], 'test.png', {
+ type: 'image/png',
+ } );
+
+ mockRequestParams = {
+ imageFile,
+ };
+ mockedRequestJetpackToken.mockResolvedValue( { token: 'fake_token' } );
+ } );
+
+ it( 'should initialize with correct default values', () => {
+ const { result } = renderHook( () => useBackgroundRemoval() );
+ expect( result.current.imageData ).toBeNull();
+ expect( result.current.loading ).toBeFalsy();
+ } );
+
+ it( 'should return error on empty token', async () => {
+ mockedRequestJetpackToken.mockResolvedValue( { token: '' } );
+ const { result } = renderHook( () => useBackgroundRemoval() );
+ await expect(
+ act( async () => {
+ await result.current.fetchImage( mockRequestParams );
+ } )
+ ).rejects.toThrow( 'Invalid token' );
+ } );
+
+ it( 'should handle invalid file type', async () => {
+ mockRequestParams.imageFile = new File(
+ [ new ArrayBuffer( 51200 ) ],
+ 'test.txt',
+ { type: 'text/plain' }
+ );
+ const { result } = renderHook( () => useBackgroundRemoval() );
+ await expect(
+ act( async () => {
+ await result.current.fetchImage( mockRequestParams );
+ } )
+ ).rejects.toThrow( 'Invalid image file' );
+ } );
+
+ it( 'should return error on image file too small', async () => {
+ mockRequestParams.imageFile = new File(
+ [ new ArrayBuffer( 1024 ) ],
+ 'test.png',
+ { type: 'image/png' }
+ ); // 1KB
+ const { result } = renderHook( () => useBackgroundRemoval() );
+ await expect(
+ act( async () => {
+ await result.current.fetchImage( mockRequestParams );
+ } )
+ ).rejects.toThrow( 'Image file too small, must be at least 5KB' );
+ } );
+
+ it( 'should return error on image file too large', async () => {
+ mockRequestParams.imageFile = new File(
+ [ new ArrayBuffer( 10240 * 1024 * 2 ) ],
+ 'test.png',
+ { type: 'image/png' }
+ ); // 10MB
+ const { result } = renderHook( () => useBackgroundRemoval() );
+ await expect(
+ act( async () => {
+ await result.current.fetchImage( mockRequestParams );
+ } )
+ ).rejects.toThrow( 'Image file too large, must be under 10MB' );
+ } );
+
+ it( 'should set loading to true when fetchImage is called', async () => {
+ (
+ apiFetch as jest.MockedFunction< typeof apiFetch >
+ ).mockResolvedValue( {
+ blob: () =>
+ Promise.resolve(
+ new Blob( [ new ArrayBuffer( 51200 ) ], {
+ type: 'image/jpeg',
+ } )
+ ),
+ } );
+
+ const { result } = renderHook( () => useBackgroundRemoval() );
+ await act( async () => {
+ result.current.fetchImage( mockRequestParams );
+ await waitFor( () =>
+ expect( result.current.loading ).toBeTruthy()
+ );
+ } );
+ expect( mockedRequestJetpackToken ).toHaveBeenCalled();
+ } );
+
+ it( 'should handle successful API call', async () => {
+ (
+ apiFetch as jest.MockedFunction< typeof apiFetch >
+ ).mockResolvedValue( {
+ blob: () =>
+ Promise.resolve(
+ new Blob( [ new ArrayBuffer( 51200 ) ], {
+ type: 'image/jpeg',
+ } )
+ ),
+ } );
+
+ const { result } = renderHook( () => useBackgroundRemoval() );
+ await act( async () => {
+ await result.current.fetchImage( mockRequestParams );
+ } );
+ expect( result.current.loading ).toBe( false );
+ expect( result.current.imageData ).toBeInstanceOf( Blob );
+ } );
+
+ it( 'should handle API errors', async () => {
+ (
+ apiFetch as jest.MockedFunction< typeof apiFetch >
+ ).mockImplementation( () => {
+ throw new Error( 'API Error' );
+ } );
+
+ const { result } = renderHook( () => useBackgroundRemoval() );
+ await expect(
+ act( async () => {
+ await result.current.fetchImage( mockRequestParams );
+ } )
+ ).rejects.toThrow( 'API Error' );
+ await waitFor( () => expect( result.current.loading ).toBeFalsy() );
+ expect( result.current.imageData ).toBe( null );
+ } );
+} );
diff --git a/packages/js/ai/src/hooks/useBackgroundRemoval.ts b/packages/js/ai/src/hooks/useBackgroundRemoval.ts
new file mode 100644
index 00000000000..d120086a176
--- /dev/null
+++ b/packages/js/ai/src/hooks/useBackgroundRemoval.ts
@@ -0,0 +1,85 @@
+/**
+ * External dependencies
+ */
+import { useState } from 'react';
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Internal dependencies
+ */
+import { createExtendedError } from '../utils/create-extended-error';
+import { requestJetpackToken } from '../utils/requestJetpackToken';
+
+export type BackgroundRemovalParams = {
+ imageFile: File;
+};
+
+type BackgroundRemovalResponse = {
+ loading: boolean;
+ imageData: Blob | null;
+ fetchImage: ( params: BackgroundRemovalParams ) => Promise< Blob >;
+};
+
+export const useBackgroundRemoval = (): BackgroundRemovalResponse => {
+ const [ loading, setLoading ] = useState( false );
+ const [ imageData, setImageData ] = useState< Blob | null >( null );
+
+ const fetchImage = async ( params: BackgroundRemovalParams ) => {
+ setLoading( true );
+ const { imageFile } = params;
+ const { token } = await requestJetpackToken();
+
+ if ( ! token ) {
+ throw createExtendedError( 'Invalid token', 'invalid_jwt' );
+ }
+ // Validate that the file is an image and has actual content.
+ if ( ! imageFile.type.startsWith( 'image/' ) ) {
+ throw createExtendedError(
+ 'Invalid image file',
+ 'invalid_image_file'
+ );
+ }
+
+ const fileSizeInKB = params.imageFile.size / 1024;
+ if ( fileSizeInKB < 5 ) {
+ throw createExtendedError(
+ 'Image file too small, must be at least 5KB',
+ 'image_file_too_small'
+ );
+ }
+
+ // The WPCOM REST API endpoint has a 10MB image size limit.
+ if ( fileSizeInKB > 10240 ) {
+ throw createExtendedError(
+ 'Image file too large, must be under 10MB',
+ 'image_file_too_large'
+ );
+ }
+
+ const formData = new FormData();
+ formData.append( 'image_file', imageFile );
+ formData.append( 'token', token );
+
+ try {
+ const response = await apiFetch( {
+ url: 'https://public-api.wordpress.com/wpcom/v2/ai-background-removal',
+ method: 'POST',
+ body: formData,
+ parse: false,
+ credentials: 'omit',
+ } );
+
+ const blob = await (
+ response as { blob: () => Promise< Blob > }
+ ).blob();
+ setImageData( blob );
+ return blob;
+ } catch ( err ) {
+ throw err;
+ } finally {
+ setLoading( false );
+ }
+ };
+
+ return { loading, imageData, fetchImage };
+};
diff --git a/packages/js/ai/src/index.ts b/packages/js/ai/src/index.ts
index 489c950b08e..33144f0c8da 100644
--- a/packages/js/ai/src/index.ts
+++ b/packages/js/ai/src/index.ts
@@ -4,6 +4,7 @@
export {
useCompletion as __experimentalUseCompletion,
UseCompletionError,
+ useBackgroundRemoval as __experimentalUseBackgroundRemoval,
} from './hooks';
/**
diff --git a/packages/js/ai/src/utils/index.ts b/packages/js/ai/src/utils/index.ts
index 3fd24ee2380..e6ccc468370 100644
--- a/packages/js/ai/src/utils/index.ts
+++ b/packages/js/ai/src/utils/index.ts
@@ -1,2 +1,3 @@
export * from './text-completion';
export * from './create-extended-error';
+export * from './requestJetpackToken';
diff --git a/packages/js/ai/src/utils/requestJetpackToken.ts b/packages/js/ai/src/utils/requestJetpackToken.ts
new file mode 100644
index 00000000000..90376d0d6e6
--- /dev/null
+++ b/packages/js/ai/src/utils/requestJetpackToken.ts
@@ -0,0 +1,87 @@
+/**
+ * External dependencies
+ */
+import debugFactory from 'debug';
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Internal dependencies
+ */
+
+import { createExtendedError } from './create-extended-error';
+export const debugToken = debugFactory( 'jetpack-ai-assistant:token' );
+
+export const JWT_TOKEN_ID = 'jetpack-ai-jwt-token';
+export const JWT_TOKEN_EXPIRATION_TIME = 2 * 60 * 1000;
+
+declare global {
+ interface Window {
+ JP_CONNECTION_INITIAL_STATE: {
+ apiNonce: string;
+ siteSuffix: string;
+ connectionStatus: { isActive: boolean };
+ };
+ }
+}
+
+/**
+ * Request a token from the Jetpack site to use with the API
+ *
+ * @return {Promise<{token: string, blogId: string}>} The token and the blogId
+ */
+
+export async function requestJetpackToken() {
+ const token = localStorage.getItem( JWT_TOKEN_ID );
+ let tokenData;
+
+ if ( token ) {
+ try {
+ tokenData = JSON.parse( token );
+ } catch ( err ) {
+ debugToken( 'Error parsing token', err );
+ throw createExtendedError(
+ 'Error parsing cached token',
+ 'token_parse_error'
+ );
+ }
+ }
+
+ if ( tokenData && tokenData?.expire > Date.now() ) {
+ debugToken( 'Using cached token' );
+ return tokenData;
+ }
+
+ const apiNonce = window.JP_CONNECTION_INITIAL_STATE?.apiNonce;
+ const siteSuffix = window.JP_CONNECTION_INITIAL_STATE?.siteSuffix;
+
+ try {
+ const data: { token: string; blog_id: string } = await apiFetch( {
+ path: '/jetpack/v4/jetpack-ai-jwt?_cacheBuster=' + Date.now(),
+ credentials: 'same-origin',
+ headers: {
+ 'X-WP-Nonce': apiNonce,
+ },
+ method: 'POST',
+ } );
+
+ const newTokenData = {
+ token: data.token,
+ blogId: siteSuffix,
+
+ /**
+ * Let's expire the token in 2 minutes
+ */
+ expire: Date.now() + JWT_TOKEN_EXPIRATION_TIME,
+ };
+
+ debugToken( 'Storing new token' );
+ localStorage.setItem( JWT_TOKEN_ID, JSON.stringify( newTokenData ) );
+
+ return newTokenData;
+ } catch ( e ) {
+ throw createExtendedError(
+ 'Error fetching new token',
+ 'token_fetch_error'
+ );
+ }
+}
diff --git a/packages/js/ai/src/utils/text-completion.ts b/packages/js/ai/src/utils/text-completion.ts
index 0512c832990..9249e154730 100644
--- a/packages/js/ai/src/utils/text-completion.ts
+++ b/packages/js/ai/src/utils/text-completion.ts
@@ -1,89 +1,7 @@
-/**
- * External dependencies
- */
-import apiFetch from '@wordpress/api-fetch';
-import debugFactory from 'debug';
-
/**
* Internal dependencies
*/
-import { createExtendedError } from './create-extended-error';
-
-const debugToken = debugFactory( 'jetpack-ai-assistant:token' );
-
-const JWT_TOKEN_ID = 'jetpack-ai-jwt-token';
-const JWT_TOKEN_EXPIRATION_TIME = 2 * 60 * 1000;
-
-declare global {
- interface Window {
- JP_CONNECTION_INITIAL_STATE: {
- apiNonce: string;
- siteSuffix: string;
- connectionStatus: { isActive: boolean };
- };
- }
-}
-
-/**
- * Request a token from the Jetpack site to use with the API
- *
- * @return {Promise<{token: string, blogId: string}>} The token and the blogId
- */
-export async function requestJetpackToken() {
- const token = localStorage.getItem( JWT_TOKEN_ID );
- let tokenData;
-
- if ( token ) {
- try {
- tokenData = JSON.parse( token );
- } catch ( err ) {
- debugToken( 'Error parsing token', err );
- throw createExtendedError(
- 'Error parsing cached token',
- 'token_parse_error'
- );
- }
- }
-
- if ( tokenData && tokenData?.expire > Date.now() ) {
- debugToken( 'Using cached token' );
- return tokenData;
- }
-
- const apiNonce = window.JP_CONNECTION_INITIAL_STATE?.apiNonce;
- const siteSuffix = window.JP_CONNECTION_INITIAL_STATE?.siteSuffix;
-
- try {
- const data: { token: string; blog_id: string } = await apiFetch( {
- path: '/jetpack/v4/jetpack-ai-jwt?_cacheBuster=' + Date.now(),
- credentials: 'same-origin',
- headers: {
- 'X-WP-Nonce': apiNonce,
- },
- method: 'POST',
- } );
-
- const newTokenData = {
- token: data.token,
- blogId: siteSuffix,
-
- /**
- * Let's expire the token in 2 minutes
- */
- expire: Date.now() + JWT_TOKEN_EXPIRATION_TIME,
- };
-
- debugToken( 'Storing new token' );
- localStorage.setItem( JWT_TOKEN_ID, JSON.stringify( newTokenData ) );
-
- return newTokenData;
- } catch ( e ) {
- throw createExtendedError(
- 'Error fetching new token',
- 'token_fetch_error'
- );
- }
-}
+import { requestJetpackToken } from './requestJetpackToken';
/**
* Leaving this here to make it easier to debug the streaming API calls for now
diff --git a/plugins/woo-ai/changelog/add-background-removal-add-image-count-40743 b/plugins/woo-ai/changelog/add-background-removal-add-image-count-40743
new file mode 100644
index 00000000000..3023b47fdbf
--- /dev/null
+++ b/plugins/woo-ai/changelog/add-background-removal-add-image-count-40743
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding number of images to tracks events for background removal.
diff --git a/plugins/woo-ai/changelog/add-background-removal-feedback-39730 b/plugins/woo-ai/changelog/add-background-removal-feedback-39730
new file mode 100644
index 00000000000..33d2e80af0d
--- /dev/null
+++ b/plugins/woo-ai/changelog/add-background-removal-feedback-39730
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding feedback snackbar after image background removal
diff --git a/plugins/woo-ai/changelog/add-background-removal-spotlight-40495 b/plugins/woo-ai/changelog/add-background-removal-spotlight-40495
new file mode 100644
index 00000000000..fcb13fc36b0
--- /dev/null
+++ b/plugins/woo-ai/changelog/add-background-removal-spotlight-40495
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding spotlight to bring attention to background removal link on Media Library.
diff --git a/plugins/woo-ai/changelog/add-remove-background-ui-40440 b/plugins/woo-ai/changelog/add-remove-background-ui-40440
new file mode 100644
index 00000000000..c49c9a7d139
--- /dev/null
+++ b/plugins/woo-ai/changelog/add-remove-background-ui-40440
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding background removal for legacy product editor images.
diff --git a/plugins/woo-ai/src/components/index.ts b/plugins/woo-ai/src/components/index.ts
index d2c6cdcde20..5ef4d294ec1 100644
--- a/plugins/woo-ai/src/components/index.ts
+++ b/plugins/woo-ai/src/components/index.ts
@@ -2,4 +2,4 @@ export * from './random-loading-message';
export * from './description-completion-buttons';
export * from './magic-button';
export * from './suggestion-pills';
-export * from './info-modal';
+export * from './tour-spotlight';
diff --git a/plugins/woo-ai/src/components/info-modal/index.ts b/plugins/woo-ai/src/components/info-modal/index.ts
deleted file mode 100644
index 3826c03df5e..00000000000
--- a/plugins/woo-ai/src/components/info-modal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './info-modal';
diff --git a/plugins/woo-ai/src/components/info-modal/info-modal.scss b/plugins/woo-ai/src/components/info-modal/info-modal.scss
deleted file mode 100644
index 5ee9e113441..00000000000
--- a/plugins/woo-ai/src/components/info-modal/info-modal.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-.shortDescriptionGenerated-spotlight {
- max-width: 400px;
- padding:20px;
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
-
- span {
- font-weight: 600;
- font-size: 18px;
- }
-
- p {
- font-size:16px;
- margin-bottom: 20px;
- }
-
- button {
- padding: 10px 15px;
- border-radius: 5px;
- color:#fff;
- background-color: #0074a2;
- border: 1px solid lightblue;
- float:right;
- border:none;
- margin-bottom:20px;
-
- &:hover, &:focus {
- background-color: darken(#0074a2, 10%); // Slightly darken the blue on hover and focus
- }
-
- &:active {
- background-color: darken(#0074a2, 15%); // Darken a bit more on active state
- }
- }
-}
\ No newline at end of file
diff --git a/plugins/woo-ai/src/components/info-modal/info-modal.tsx b/plugins/woo-ai/src/components/info-modal/info-modal.tsx
deleted file mode 100644
index 15f2d8c9648..00000000000
--- a/plugins/woo-ai/src/components/info-modal/info-modal.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * External dependencies
- */
-import { useDispatch, select } from '@wordpress/data';
-import { store as preferencesStore } from '@wordpress/preferences';
-import React from 'react';
-
-/**
- * Internal dependencies
- */
-import TourSpotlight from '../tour-spotlight/tour-spotlight';
-import './info-modal.scss';
-
-interface InfoModalProps {
- id: string;
- message: string;
- title: string;
-}
-
-export const InfoModal: React.FC< InfoModalProps > = ( {
- id,
- message,
- title,
-} ) => {
- const anchorElement = document.querySelector( '#postexcerpt' );
- const hasBeenDismissedBefore = select( preferencesStore ).get(
- 'woo-ai-plugin',
- `modalDismissed-${ id }`
- );
-
- const { set } = useDispatch( preferencesStore );
-
- if ( ! anchorElement || hasBeenDismissedBefore ) return null;
-
- const closeTour = () => {
- set( 'woo-ai-plugin', `modalDismissed-${ id }`, true );
- };
-
- return (
-
- { - config.steps[ currentStepIndex ].meta - .description - } -
- -