Migrate anonymous resources to authenticated Customer accounts

Learn how to seamlessly migrate anonymous user data, such as Carts and Shopping Lists, to an authenticated Customer account upon sign-in or sign-up.

After completing this page, you should be able to:

  • Implement automatic Cart migration during sign-in by selecting the appropriate anonymousCartSignInMode option.

  • Develop a manual process to transfer ownership of anonymous resources, such as Shopping Lists, to an authenticated Customer.

When a user interacts with your storefront without being logged in, Composable Commerce tracks their activities such as adding items to a Cart by using an anonymousId. This ID is typically stored in either a client-side cookie or local storage. Although Carts are the most common resource created anonymously, other resources such as Custom Objects and Shopping Lists can also be linked to an anonymousId.
If the user signs up or logs in, then you must migrate these anonymous resources to their authenticated Customer account (customerId). Failing to do so results in a poor user experience because the user's Cart or other data appears to be lost upon login.

Automatic Cart migration

For Carts, Composable Commerce provides a built-in mechanism to automate migration during sign-in or sign-up, which simplifies development for this common use case.

During sign-in and sign-up

To trigger automatic migration, provide the anonymousCartId in the request body of a POST /{projectKey}/login or POST /{projectKey}/customers request. The anonymousCartSignInMode field (for login) and the presence of anonymousCartId (for sign-up) control how the anonymous Cart is handled.

anonymousCartSignInMode for login

This field in the POST /{projectKey}/login request dictates how to integrate the anonymous Cart with the Customer's account.
  • MergeWithExistingCustomerCart (default): This mode preserves all items from both the anonymous and any existing Customer Carts.
    • Scenario: Anonymous user has Cart A; Customer has an existing active Cart B.
      • Result: Line Items from Cart A are merged into Cart B. Cart B remains the active Cart for the logged-in Customer. Cart A is typically deleted or marked as Merged.
      • Conflict resolution: by default, if a Product Variant with identical Custom Fields exists in both Carts, then the higher quantity is retained. Otherwise, the item is added as a new Line Item.
    • Scenario: Anonymous user has Cart A; Customer has no active Cart.
      • Result: Cart A is assigned to the Customer and becomes their active Cart.
  • UseAsNewActiveCustomerCart: This mode replaces an existing Customer Cart with an anonymous Cart.
    • Scenario: Anonymous user has Cart A; Customer has an existing active Cart B.
      • Result: Cart A becomes the Customer's active Cart. Cart B remains in an active state, but is hidden in a single Cart scenario. To avoid confusion and prevent Cart B from reappearing after Cart A becomes an Order, you should consider deleting Cart B.
      • Use case: this is useful if the anonymous Cart is considered more current or if you want to override an older, stale customer Cart.
    • Scenario: Anonymous user has Cart A; Customer has no active Cart.
      • Result: Cart A is assigned to the Customer, which is the same outcome as MergeWithExistingCustomerCart in this case.

Sign-up

When anonymousCartId is provided in a POST /{projectKey}/customers request, the Cart is automatically assigned to the new Customer. No anonymousCartSignInMode is needed because there's no existing Customer Cart to merge with.

Example: Merge Carts on login

The following example shows how to log in a Customer and merge their anonymous Cart with an existing Customer Cart:

import { apiRoot } from '../../client'; // Assuming apiRoot is initialized and exported here
import { CustomerSignInResult } from '@commercetools/platform-sdk'; // Import the type for the response body

// Function to handle login with anonymous cart merge
async function customerLoginWithCartMergeInStore(
  storeKey: string,
  email: string,
  password: string,
  anonymousCartId: string
): Promise<CustomerSignInResult> {
  try {
    const loginResponse = await apiRoot
      .inStoreKeyWithStoreKeyValue({ storeKey: storeKey })
      .login()
      .post({
        body: {
          email: email,
          password: password,
          anonymousCartId: anonymousCartId,
          anonymousCartSignInMode: 'MergeWithExistingCustomerCart', // Explicitly setting, though it's default
        },
      })
      .execute();

    console.log('Login successful with cart merge for store "${storeKey}"!');
    console.log('Customer details:', loginResponse.body.customer.email);
    console.log('Merged cart details:', loginResponse.body.cart);
    return loginResponse.body;
  } catch (error: any) {
    // Type 'any' for caught error for now, as stricter typing for HTTP errors can be complex
    console.error(
      'Login failed with cart merge for store "${storeKey}":',
      error
    );
    // You might add more specific error handling here based on Composable Commerce error codes
    // For example, if (error.statusCode === 400 && error.body && error.body.errors.some(e => e.code === 'InvalidCredentials')) { ... }
    throw error;
  }
}

// Example login for David with anonymous cart, in a store
(async () => {
  const davidEmail = 'david.shopper@example.com'; // Assuming David's email
  const davidPassword = 'DavidSecurePass!'; // Assuming David's password
  const davidAnonymousCartId = 'anonymous-cart-id-789'; // This ID would come from the client-side
  const storeKey = 'electronics-high-tech-store';

  console.log(
    `\n--- Attempting store login for David with anonymous cart (${davidAnonymousCartId}) ---`
  );
  try {
    const loginResult = await customerLoginWithCartMergeInStore(
      storeKey,
      davidEmail,
      davidPassword,
      davidAnonymousCartId
    );
    console.log(
      `David's carts successfully merged upon store login. New active cart ID: ${loginResult.cart?.id}`
    );
  } catch (err) {
    console.error('Failed to log in David and merge cart in the store.');
  }
})();

Key takeaways

  • Automatic for Carts: Composable Commerce automates Cart migration during login and sign-up.
  • Trigger with anonymousCartId: pass the anonymousCartId in the login or sign-up request to initiate migration.
  • Control with anonymousCartSignInMode: use MergeWithExistingCustomerCart (default) to combine Carts or UseAsNewActiveCustomerCart to replace the Customer's Cart with an anonymous Cart.
  • Sign-up is simpler: during sign-up, an anonymous Cart is directly assigned to the new Customer without needing a merge mode.

Manual resource migration

Although Carts can be migrated automatically, other resources associated with an anonymousId must be migrated manually. This is a critical responsibility of your application's backend or backend for frontend (BFF).

Resources that require manual migration

  • Custom Objects and Custom Fields: any custom data linked to an anonymousId, such as user preferences or browsing history, requires manual migration.
  • Shopping Lists: Shopping Lists associated with an anonymousId aren't automatically transferred.
  • Specific Line Items: under certain conditions, such as a missing shippingAddress on the Customer's Cart, Line Items from the anonymous Cart might not be merged automatically. For more information, see the Cart merge during sign-in documentation.

Manual migration process

Your application must implement the following logic to transfer ownership of these resources:

  1. Identify anonymous resources: before authentication, query for any resources associated with the anonymousId from the client-side session. For example, you can query Shopping Lists or Custom Objects that store the anonymousId.
  2. Authenticate user: after a successful sign-in or sign-up, obtain the customerId details.
  3. Update resources: use the appropriate update actions to re-associate the anonymous resources with the customerId.
    • Shopping Lists: use the setCustomer update action.
    • Custom Objects: update a field in the object to reference the customerId or recreate the object with a new key or container that includes the customerId.
  4. Clean up: after migration, remove the anonymousId from the client-side session and consider deleting the original anonymous resources to maintain data hygiene.

Example - Assign a Shopping List to a Customer

import { apiRoot } from '../../client'; // Assuming apiRoot is initialized and exported here
import { ShoppingList, ShoppingListUpdate } from '@commercetools/platform-sdk'; // Import necessary types

// Function to assign a Shopping List to a customer in a store
async function assignShoppingListToCustomerInStore(
  storeKey: string,
  shoppingListId: string,
  shoppingListVersion: number,
  customerId: string
): Promise<ShoppingList> {
  try {
    const shoppingListUpdate: ShoppingListUpdate = {
      version: shoppingListVersion,
      actions: [
        {
          action: 'setCustomer',
          customer: {
            typeId: 'customer',
            id: customerId,
          },
        },
      ],
    };

    const updatedShoppingListResponse = await apiRoot
      .inStoreKeyWithStoreKeyValue({ storeKey: storeKey })
      .shoppingLists()
      .withId({ ID: shoppingListId })
      .post({ body: shoppingListUpdate })
      .execute();

    const updatedShoppingList: ShoppingList = updatedShoppingListResponse.body;
    console.log(
      `Shopping List ${shoppingListId} successfully assigned to customer ${customerId} within store "${storeKey}".`
    );
    return updatedShoppingList;
  } catch (error: any) {
    console.error(
      `Error assigning Shopping List ${shoppingListId} to customer ${customerId} in store "${storeKey}":`,
      error
    );
    throw error;
  }
}

// Example usage after David logs in
(async () => {
  const ELECTRONICS_HIGH_TECH_STORE_KEY = 'electronics-high-tech-store';
  const davidCustomerId = 'david-customer-id'; // Obtained from successful login

  // Assume you have an anonymous Shopping List ID and its current version,
  // which was associated with 'electronics-high-tech-store'
  const anonymousShoppingListId = 'anon-shoppingList-id-123'; // Replace with a real ID
  const anonymousShoppingListVersion = 1; // Replace with a real version

  console.log(
    `\n--- Assigning anonymous Shopping List to customer ${davidCustomerId} for store "${ELECTRONICS_HIGH_TECH_STORE_KEY}" ---`
  );
  try {
    await assignShoppingListToCustomerInStore(
      ELECTRONICS_HIGH_TECH_STORE_KEY,
      anonymousShoppingListId,
      anonymousShoppingListVersion,
      davidCustomerId
    );
    console.log(
      `Anonymous Shopping List successfully migrated to David's account within ${ELECTRONICS_HIGH_TECH_STORE_KEY}.`
    );
  } catch (err) {
    console.error('Failed to migrate anonymous Shopping List in-store.');
  }
})();

Key takeaways

  • Manual migration is required: resources such as Shopping Lists and Custom Objects aren't migrated automatically.
  • Application responsibility: your application's backend or BFF must implement the logic for manual migration.
  • Process: the process involves identifying anonymous resources, authenticating the user, updating the resources with the customerId, and cleaning up old anonymous data.
  • Flexibility: manual migration enables you to implement custom business logic for merging or transferring data beyond what the automatic process provides.

You now have a solid understanding of how to manage the transition from anonymous browsing to authenticated Customer sessions, both automatically for carts and manually for other vital resources. This ensures a smooth and continuous shopping journey for your Customers.

Entire process workflow

Here's a flowchart diagram that illustrates the complete process of migrating anonymous resources to authenticated Customer accounts:

Test your knowledge