diff --git a/src/LiveLocationManager.ts b/src/LiveLocationManager.ts
new file mode 100644
index 000000000..49df7c157
--- /dev/null
+++ b/src/LiveLocationManager.ts
@@ -0,0 +1,297 @@
+/**
+ * RULES:
+ *
+ * 1. one loc-sharing message per channel per user
+ * 2. live location is intended to be per device
+ * but created_by_device_id has currently no checks,
+ * and user can update the location from another device
+ * thus making location sharing based on user and channel
+ */
+
+import { withCancellation } from './utils/concurrency';
+import { StateStore } from './store';
+import { WithSubscriptions } from './utils/WithSubscriptions';
+import type { StreamChat } from './client';
+import type { Unsubscribe } from './store';
+import type {
+  EventTypes,
+  MessageResponse,
+  SharedLiveLocationResponse,
+  SharedLocationResponse,
+} from './types';
+import type { Coords } from './messageComposer';
+
+export type WatchLocationHandler = (value: Coords) => void;
+export type WatchLocation = (handler: WatchLocationHandler) => Unsubscribe;
+type DeviceIdGenerator = () => string;
+type MessageId = string;
+
+export type ScheduledLiveLocationSharing = SharedLiveLocationResponse & {
+  stopSharingTimeout: ReturnType<typeof setTimeout> | null;
+};
+
+export type LiveLocationManagerState = {
+  ready: boolean;
+  messages: Map<MessageId, ScheduledLiveLocationSharing>;
+};
+
+const isExpiredLocation = (location: SharedLiveLocationResponse) => {
+  const endTimeTimestamp = new Date(location.end_at).getTime();
+
+  return endTimeTimestamp < Date.now();
+};
+
+function isValidLiveLocationMessage(
+  message?: MessageResponse,
+): message is MessageResponse & { shared_location: SharedLiveLocationResponse } {
+  if (!message || message.type === 'deleted' || !message.shared_location?.end_at)
+    return false;
+
+  return !isExpiredLocation(message.shared_location as SharedLiveLocationResponse);
+}
+
+export type LiveLocationManagerConstructorParameters = {
+  client: StreamChat;
+  getDeviceId: DeviceIdGenerator;
+  watchLocation: WatchLocation;
+};
+
+// Hard-coded minimal throttle timeout
+export const UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT = 3000;
+
+export class LiveLocationManager extends WithSubscriptions {
+  public state: StateStore<LiveLocationManagerState>;
+  private client: StreamChat;
+  private getDeviceId: DeviceIdGenerator;
+  private _deviceId: string;
+  private watchLocation: WatchLocation;
+
+  static symbol = Symbol(LiveLocationManager.name);
+
+  constructor({
+    client,
+    getDeviceId,
+    watchLocation,
+  }: LiveLocationManagerConstructorParameters) {
+    if (!client.userID) {
+      throw new Error('Live-location sharing is reserved for client-side use only');
+    }
+
+    super();
+
+    this.client = client;
+    this.state = new StateStore<LiveLocationManagerState>({
+      messages: new Map(),
+      ready: false,
+    });
+    this._deviceId = getDeviceId();
+    this.getDeviceId = getDeviceId;
+    this.watchLocation = watchLocation;
+  }
+
+  public async init() {
+    await this.assureStateInit();
+    this.registerSubscriptions();
+  }
+
+  public registerSubscriptions = () => {
+    this.incrementRefCount();
+    if (this.hasSubscriptions) return;
+
+    this.addUnsubscribeFunction(this.subscribeLiveLocationSharingUpdates());
+    this.addUnsubscribeFunction(this.subscribeTargetMessagesChange());
+  };
+
+  public unregisterSubscriptions = () => super.unregisterSubscriptions();
+
+  get messages() {
+    return this.state.getLatestValue().messages;
+  }
+
+  get stateIsReady() {
+    return this.state.getLatestValue().ready;
+  }
+
+  get deviceId() {
+    if (!this._deviceId) {
+      this._deviceId = this.getDeviceId();
+    }
+    return this._deviceId;
+  }
+
+  private async assureStateInit() {
+    if (this.stateIsReady) return;
+    const { active_live_locations } = await this.client.getSharedLocations();
+    this.state.next({
+      messages: new Map(
+        active_live_locations
+          .filter((location) => !isExpiredLocation(location))
+          .map((location) => [
+            location.message_id,
+            {
+              ...location,
+              stopSharingTimeout: setTimeout(
+                () => {
+                  this.unregisterMessages([location.message_id]);
+                },
+                new Date(location.end_at).getTime() - Date.now(),
+              ),
+            },
+          ]),
+      ),
+      ready: true,
+    });
+  }
+
+  private subscribeTargetMessagesChange() {
+    let unsubscribeWatchLocation: null | (() => void) = null;
+
+    // Subscribe to location updates only if there are relevant messages to
+    // update, no need for the location watcher to be active/instantiated otherwise
+    const unsubscribe = this.state.subscribeWithSelector(
+      ({ messages }) => ({ messages }),
+      ({ messages }) => {
+        if (!messages.size) {
+          unsubscribeWatchLocation?.();
+          unsubscribeWatchLocation = null;
+        } else if (messages.size && !unsubscribeWatchLocation) {
+          unsubscribeWatchLocation = this.subscribeWatchLocation();
+        }
+      },
+    );
+
+    return () => {
+      unsubscribe();
+      unsubscribeWatchLocation?.();
+    };
+  }
+
+  private subscribeWatchLocation() {
+    let nextAllowedUpdateCallTimestamp = Date.now();
+
+    const unsubscribe = this.watchLocation(({ latitude, longitude }) => {
+      // Integrators can adjust the update interval by supplying custom watchLocation subscription,
+      // but the minimal timeout still has to be set as a failsafe (to prevent rate-limitting)
+      if (Date.now() < nextAllowedUpdateCallTimestamp) return;
+
+      nextAllowedUpdateCallTimestamp =
+        Date.now() + UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT;
+
+      withCancellation(LiveLocationManager.symbol, async () => {
+        const promises: Promise<SharedLocationResponse>[] = [];
+        await this.assureStateInit();
+        const expiredLocations: string[] = [];
+
+        for (const [messageId, location] of this.messages) {
+          if (isExpiredLocation(location)) {
+            expiredLocations.push(location.message_id);
+            continue;
+          }
+          if (location.latitude === latitude && location.longitude === longitude)
+            continue;
+          const promise = this.client.updateLocation({
+            created_by_device_id: location.created_by_device_id,
+            message_id: messageId,
+            latitude,
+            longitude,
+          });
+
+          promises.push(promise);
+        }
+        this.unregisterMessages(expiredLocations);
+        if (promises.length > 0) {
+          await Promise.allSettled(promises);
+        }
+        // TODO: handle values (remove failed - based on specific error code), keep re-trying others
+      });
+    });
+
+    return unsubscribe;
+  }
+
+  private subscribeLiveLocationSharingUpdates() {
+    /**
+     * Both message.updated & live_location_sharing.stopped get emitted when message gets an
+     * update, live_location_sharing.stopped gets emitted only locally and only if the update goes
+     * through, it's a failsafe for when channel is no longer being watched for whatever reason
+     */
+    const subscriptions = [
+      ...(
+        [
+          'live_location_sharing.started',
+          'message.updated',
+          'message.deleted',
+        ] as EventTypes[]
+      ).map((eventType) =>
+        this.client.on(eventType, (event) => {
+          if (!event.message) return;
+
+          if (event.type === 'live_location_sharing.started') {
+            this.registerMessage(event.message);
+          } else if (event.type === 'message.updated') {
+            const isRegistered = this.messages.has(event.message.id);
+            if (isRegistered && !isValidLiveLocationMessage(event.message)) {
+              this.unregisterMessages([event.message.id]);
+            }
+            this.registerMessage(event.message);
+          } else {
+            this.unregisterMessages([event.message.id]);
+          }
+        }),
+      ),
+      this.client.on('live_location_sharing.stopped', (event) => {
+        if (!event.live_location) return;
+
+        this.unregisterMessages([event.live_location?.message_id]);
+      }),
+    ];
+
+    return () => subscriptions.forEach((subscription) => subscription.unsubscribe());
+  }
+
+  private registerMessage(message: MessageResponse) {
+    if (
+      !this.client.userID ||
+      message?.user?.id !== this.client.userID ||
+      !isValidLiveLocationMessage(message)
+    )
+      return;
+
+    this.state.next((currentValue) => {
+      const messages = new Map(currentValue.messages);
+      messages.set(message.id, {
+        ...message.shared_location,
+        stopSharingTimeout: setTimeout(
+          () => {
+            this.unregisterMessages([message.id]);
+          },
+          new Date(message.shared_location.end_at).getTime() - Date.now(),
+        ),
+      });
+      return {
+        ...currentValue,
+        messages,
+      };
+    });
+  }
+
+  private unregisterMessages(messageIds: string[]) {
+    const messages = this.messages;
+    const removedMessages = new Set(messageIds);
+    const newMessages = new Map(
+      Array.from(messages).filter(([messageId, location]) => {
+        if (removedMessages.has(messageId) && location.stopSharingTimeout) {
+          clearTimeout(location.stopSharingTimeout);
+          location.stopSharingTimeout = null;
+        }
+        return !removedMessages.has(messageId);
+      }),
+    );
+
+    if (newMessages.size === messages.size) return;
+
+    this.state.partialNext({
+      messages: newMessages,
+    });
+  }
+}
diff --git a/src/channel.ts b/src/channel.ts
index de792803d..a0a5e9f25 100644
--- a/src/channel.ts
+++ b/src/channel.ts
@@ -31,6 +31,7 @@ import type {
   GetMultipleMessagesAPIResponse,
   GetReactionsAPIResponse,
   GetRepliesAPIResponse,
+  LiveLocationPayload,
   LocalMessage,
   MarkReadOptions,
   MarkUnreadOptions,
@@ -63,10 +64,12 @@ import type {
   SendMessageAPIResponse,
   SendMessageOptions,
   SendReactionOptions,
+  StaticLocationPayload,
   TruncateChannelAPIResponse,
   TruncateOptions,
   UpdateChannelAPIResponse,
   UpdateChannelOptions,
+  UpdateLocationPayload,
   UserResponse,
 } from './types';
 import type { Role } from './permissions';
@@ -669,6 +672,37 @@ export class Channel {
     return data;
   }
 
+  public async sendSharedLocation(
+    location: StaticLocationPayload | LiveLocationPayload,
+    userId?: string,
+  ) {
+    const result = await this.sendMessage({
+      id: location.message_id,
+      shared_location: location,
+      user: userId ? { id: userId } : undefined,
+    });
+
+    if ((location as LiveLocationPayload).end_at) {
+      this.getClient().dispatchEvent({
+        message: result.message,
+        type: 'live_location_sharing.started',
+      });
+    }
+
+    return result;
+  }
+
+  public async stopLiveLocationSharing(payload: UpdateLocationPayload) {
+    const location = await this.getClient().updateLocation({
+      ...payload,
+      end_at: new Date().toISOString(),
+    });
+    this.getClient().dispatchEvent({
+      live_location: location,
+      type: 'live_location_sharing.stopped',
+    });
+  }
+
   /**
    * delete - Delete the channel. Messages are permanently removed.
    *
diff --git a/src/client.ts b/src/client.ts
index ffc030541..f83ce5042 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -191,7 +191,6 @@ import type {
   SegmentTargetsResponse,
   SegmentType,
   SendFileAPIResponse,
-  SharedLocationRequest,
   SharedLocationResponse,
   SortParam,
   StreamChatOptions,
@@ -209,6 +208,7 @@ import type {
   UpdateChannelTypeResponse,
   UpdateCommandOptions,
   UpdateCommandResponse,
+  UpdateLocationPayload,
   UpdateMessageAPIResponse,
   UpdateMessageOptions,
   UpdatePollAPIResponse,
@@ -4584,11 +4584,11 @@ export class StreamChat {
   /**
    * updateLocation - Updates a location
    *
-   * @param location UserLocation the location data to update
+   * @param location SharedLocationRequest the location data to update
    *
-   * @returns {Promise<APIResponse>} The server response
+   * @returns {Promise<SharedLocationResponse>} The server response
    */
-  async updateLocation(location: SharedLocationRequest) {
+  async updateLocation(location: UpdateLocationPayload) {
     return await this.put<SharedLocationResponse>(
       this.baseURL + `/users/live_locations`,
       location,
diff --git a/src/events.ts b/src/events.ts
index bbd8542ac..78861d5e6 100644
--- a/src/events.ts
+++ b/src/events.ts
@@ -63,6 +63,8 @@ export const EVENT_MAP = {
   'connection.recovered': true,
   'transport.changed': true,
   'capabilities.changed': true,
+  'live_location_sharing.started': true,
+  'live_location_sharing.stopped': true,
 
   // Reminder events
   'reminder.created': true,
diff --git a/src/index.ts b/src/index.ts
index e32209a55..202df56fb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -32,6 +32,7 @@ export * from './token_manager';
 export * from './types';
 export * from './channel_manager';
 export * from './offline-support';
+export * from './LiveLocationManager';
 // Don't use * here, that can break module augmentation https://github.com/microsoft/TypeScript/issues/46617
 export type {
   CustomAttachmentData,
diff --git a/src/messageComposer/LocationComposer.ts b/src/messageComposer/LocationComposer.ts
new file mode 100644
index 000000000..438d37e35
--- /dev/null
+++ b/src/messageComposer/LocationComposer.ts
@@ -0,0 +1,94 @@
+import { StateStore } from '../store';
+import type { MessageComposer } from './messageComposer';
+import type {
+  DraftMessage,
+  LiveLocationPayload,
+  LocalMessage,
+  StaticLocationPayload,
+} from '../types';
+
+export type Coords = { latitude: number; longitude: number };
+
+export type LocationComposerOptions = {
+  composer: MessageComposer;
+  message?: DraftMessage | LocalMessage;
+};
+
+export type StaticLocationPreview = StaticLocationPayload;
+
+export type LiveLocationPreview = Omit<LiveLocationPayload, 'end_at'> & {
+  durationMs?: number;
+};
+
+export type LocationComposerState = {
+  location: StaticLocationPreview | LiveLocationPreview | null;
+};
+
+const MIN_LIVE_LOCATION_SHARE_DURATION = 60 * 1000; // 1 minute;
+
+const initState = ({
+  message,
+}: {
+  message?: DraftMessage | LocalMessage;
+}): LocationComposerState => ({
+  location: message?.shared_location ?? null,
+});
+
+export class LocationComposer {
+  readonly state: StateStore<LocationComposerState>;
+  readonly composer: MessageComposer;
+  private _deviceId: string;
+
+  constructor({ composer, message }: LocationComposerOptions) {
+    this.composer = composer;
+    this.state = new StateStore<LocationComposerState>(initState({ message }));
+    this._deviceId = this.config.getDeviceId();
+  }
+
+  get config() {
+    return this.composer.config.location;
+  }
+
+  get deviceId() {
+    return this._deviceId;
+  }
+
+  get location() {
+    return this.state.getLatestValue().location;
+  }
+
+  get validLocation(): StaticLocationPayload | LiveLocationPayload | null {
+    const { durationMs, ...location } = (this.location ?? {}) as LiveLocationPreview;
+    if (
+      !!location?.created_by_device_id &&
+      location.message_id &&
+      location.latitude &&
+      location.longitude &&
+      (typeof durationMs === 'undefined' ||
+        durationMs >= MIN_LIVE_LOCATION_SHARE_DURATION)
+    ) {
+      return {
+        ...location,
+        end_at: durationMs && new Date(Date.now() + durationMs).toISOString(),
+      } as StaticLocationPayload | LiveLocationPayload;
+    }
+    return null;
+  }
+
+  initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => {
+    this.state.next(initState({ message }));
+  };
+
+  setData = (data: { durationMs?: number } & Coords) => {
+    if (!this.config.enabled) return;
+    if (!data.latitude || !data.longitude) return;
+
+    this.state.partialNext({
+      location: {
+        ...data,
+        message_id: this.composer.id,
+        created_by_device_id: this.deviceId,
+      },
+    });
+  };
+}
diff --git a/src/messageComposer/attachmentIdentity.ts b/src/messageComposer/attachmentIdentity.ts
index 87a448280..d775948c3 100644
--- a/src/messageComposer/attachmentIdentity.ts
+++ b/src/messageComposer/attachmentIdentity.ts
@@ -1,4 +1,4 @@
-import type { Attachment } from '../types';
+import type { Attachment, SharedLocationResponse } from '../types';
 import type {
   AudioAttachment,
   FileAttachment,
@@ -90,3 +90,10 @@ export const isUploadedAttachment = (
   isImageAttachment(attachment) ||
   isVideoAttachment(attachment) ||
   isVoiceRecordingAttachment(attachment);
+
+export const isSharedLocationResponse = (
+  location: unknown,
+): location is SharedLocationResponse =>
+  !!(location as SharedLocationResponse).latitude &&
+  !!(location as SharedLocationResponse).longitude &&
+  !!(location as SharedLocationResponse).channel_cid;
diff --git a/src/messageComposer/configuration/configuration.ts b/src/messageComposer/configuration/configuration.ts
index 77282c9dc..0b47f5e99 100644
--- a/src/messageComposer/configuration/configuration.ts
+++ b/src/messageComposer/configuration/configuration.ts
@@ -3,9 +3,11 @@ import { API_MAX_FILES_ALLOWED_PER_MESSAGE } from '../../constants';
 import type {
   AttachmentManagerConfig,
   LinkPreviewsManagerConfig,
+  LocationComposerConfig,
   MessageComposerConfig,
 } from './types';
 import type { TextComposerConfig } from './types';
+import { generateUUIDv4 } from '../../utils';
 
 export const DEFAULT_LINK_PREVIEW_MANAGER_CONFIG: LinkPreviewsManagerConfig = {
   debounceURLEnrichmentMs: 1500,
@@ -36,9 +38,15 @@ export const DEFAULT_TEXT_COMPOSER_CONFIG: TextComposerConfig = {
   publishTypingEvents: true,
 };
 
+export const DEFAULT_LOCATION_COMPOSER_CONFIG: LocationComposerConfig = {
+  enabled: true,
+  getDeviceId: () => generateUUIDv4(),
+};
+
 export const DEFAULT_COMPOSER_CONFIG: MessageComposerConfig = {
   attachments: DEFAULT_ATTACHMENT_MANAGER_CONFIG,
   drafts: { enabled: false },
   linkPreviews: DEFAULT_LINK_PREVIEW_MANAGER_CONFIG,
+  location: DEFAULT_LOCATION_COMPOSER_CONFIG,
   text: DEFAULT_TEXT_COMPOSER_CONFIG,
 };
diff --git a/src/messageComposer/configuration/types.ts b/src/messageComposer/configuration/types.ts
index b2a3e603b..e94d17f78 100644
--- a/src/messageComposer/configuration/types.ts
+++ b/src/messageComposer/configuration/types.ts
@@ -11,6 +11,7 @@ export type UploadRequestFn = (
 export type DraftsConfiguration = {
   enabled: boolean;
 };
+
 export type TextComposerConfig = {
   /** If false, the text input, change and selection events are disabled */
   enabled: boolean;
@@ -23,6 +24,7 @@ export type TextComposerConfig = {
   /** Prevents sending a message longer than this length */
   maxLengthOnSend?: number;
 };
+
 export type AttachmentManagerConfig = {
   // todo: document removal of noFiles prop showing how to achieve the same with custom fileUploadFilter function
   /**
@@ -53,6 +55,16 @@ export type LinkPreviewsManagerConfig = {
   onLinkPreviewDismissed?: (linkPreview: LinkPreview) => void;
 };
 
+export type LocationComposerConfig = {
+  /**
+   * Allows for toggling the location addition.
+   * By default, the feature is enabled but has to be enabled also on channel level config via shared_locations.
+   */
+  enabled: boolean;
+  /** Function that provides a stable id for a device from which the location is shared */
+  getDeviceId: () => string;
+};
+
 export type MessageComposerConfig = {
   /** If true, enables creating drafts on the server */
   drafts: DraftsConfiguration;
@@ -60,6 +72,8 @@ export type MessageComposerConfig = {
   attachments: AttachmentManagerConfig;
   /** Configuration for the link previews manager */
   linkPreviews: LinkPreviewsManagerConfig;
+  /** Configuration for the location composer */
+  location: LocationComposerConfig;
   /** Maximum number of characters in a message */
   text: TextComposerConfig;
 };
diff --git a/src/messageComposer/index.ts b/src/messageComposer/index.ts
index 8e4eb92ce..59d930444 100644
--- a/src/messageComposer/index.ts
+++ b/src/messageComposer/index.ts
@@ -4,6 +4,7 @@ export * from './configuration';
 export * from './CustomDataManager';
 export * from './fileUtils';
 export * from './linkPreviewsManager';
+export * from './LocationComposer';
 export * from './messageComposer';
 export * from './middleware';
 export * from './pollComposer';
diff --git a/src/messageComposer/messageComposer.ts b/src/messageComposer/messageComposer.ts
index f23453783..65337eb75 100644
--- a/src/messageComposer/messageComposer.ts
+++ b/src/messageComposer/messageComposer.ts
@@ -1,14 +1,16 @@
 import { AttachmentManager } from './attachmentManager';
 import { CustomDataManager } from './CustomDataManager';
 import { LinkPreviewsManager } from './linkPreviewsManager';
+import { LocationComposer } from './LocationComposer';
 import { PollComposer } from './pollComposer';
 import { TextComposer } from './textComposer';
-import { DEFAULT_COMPOSER_CONFIG } from './configuration/configuration';
+import { DEFAULT_COMPOSER_CONFIG } from './configuration';
 import type { MessageComposerMiddlewareValue } from './middleware';
 import {
   MessageComposerMiddlewareExecutor,
   MessageDraftComposerMiddlewareExecutor,
 } from './middleware';
+import type { Unsubscribe } from '../store';
 import { StateStore } from '../store';
 import { formatMessage, generateUUIDv4, isLocalMessage, unformatMessage } from '../utils';
 import { mergeWith } from '../utils/mergeWith';
@@ -24,11 +26,11 @@ import type {
   MessageResponse,
   MessageResponseBase,
 } from '../types';
+import { WithSubscriptions } from '../utils/WithSubscriptions';
 import type { StreamChat } from '../client';
 import type { MessageComposerConfig } from './configuration/types';
 import type { DeepPartial } from '../types.utility';
-import type { Unsubscribe } from '../store';
-import { WithSubscriptions } from '../utils/WithSubscriptions';
+import type { MergeWithCustomizer } from '../utils/mergeWith/mergeWithCore';
 
 type UnregisterSubscriptions = Unsubscribe;
 
@@ -129,6 +131,7 @@ export class MessageComposer extends WithSubscriptions {
   linkPreviewsManager: LinkPreviewsManager;
   textComposer: TextComposer;
   pollComposer: PollComposer;
+  locationComposer: LocationComposer;
   customDataManager: CustomDataManager;
   // todo: mediaRecorder: MediaRecorderController;
 
@@ -142,10 +145,6 @@ export class MessageComposer extends WithSubscriptions {
 
     this.compositionContext = compositionContext;
 
-    this.configState = new StateStore<MessageComposerConfig>(
-      mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {}),
-    );
-
     // channel is easily inferable from the context
     if (compositionContext instanceof Channel) {
       this.channel = compositionContext;
@@ -160,6 +159,32 @@ export class MessageComposer extends WithSubscriptions {
       );
     }
 
+    const mergeChannelConfigCustomizer: MergeWithCustomizer<
+      DeepPartial<MessageComposerConfig>
+    > = (originalVal, channelConfigVal, key) =>
+      typeof originalVal === 'object'
+        ? undefined
+        : originalVal === false && key === 'enabled' // prevent enabling features that are disabled client-side
+          ? false
+          : ['string', 'number', 'bigint', 'boolean', 'symbol'].includes(
+                // prevent enabling features that are disabled server-side
+                typeof channelConfigVal,
+              )
+            ? channelConfigVal // scalar values get overridden by server-side config
+            : originalVal;
+
+    this.configState = new StateStore<MessageComposerConfig>(
+      mergeWith(
+        mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {}),
+        {
+          location: {
+            enabled: this.channel.getConfig()?.shared_locations,
+          },
+        },
+        mergeChannelConfigCustomizer,
+      ),
+    );
+
     let message: LocalMessage | DraftMessage | undefined = undefined;
     if (compositionIsDraftResponse(composition)) {
       message = composition.message;
@@ -170,6 +195,7 @@ export class MessageComposer extends WithSubscriptions {
 
     this.attachmentManager = new AttachmentManager({ composer: this, message });
     this.linkPreviewsManager = new LinkPreviewsManager({ composer: this, message });
+    this.locationComposer = new LocationComposer({ composer: this, message });
     this.textComposer = new TextComposer({ composer: this, message });
     this.pollComposer = new PollComposer({ composer: this });
     this.customDataManager = new CustomDataManager({ composer: this, message });
@@ -289,7 +315,8 @@ export class MessageComposer extends WithSubscriptions {
       (!this.attachmentManager.uploadsInProgressCount &&
         (!this.textComposer.textIsEmpty ||
           this.attachmentManager.successfulUploadsCount > 0)) ||
-      this.pollId
+      this.pollId ||
+      !!this.locationComposer.validLocation
     );
   }
 
@@ -298,7 +325,8 @@ export class MessageComposer extends WithSubscriptions {
       !this.quotedMessage &&
       this.textComposer.textIsEmpty &&
       !this.attachmentManager.attachments.length &&
-      !this.pollId
+      !this.pollId &&
+      !this.locationComposer.validLocation
     );
   }
 
@@ -320,6 +348,10 @@ export class MessageComposer extends WithSubscriptions {
 
   static generateId = generateUUIDv4;
 
+  refreshId = () => {
+    this.state.partialNext({ id: MessageComposer.generateId() });
+  };
+
   initState = ({
     composition,
   }: { composition?: DraftResponse | MessageResponse | LocalMessage } = {}) => {
@@ -333,6 +365,7 @@ export class MessageComposer extends WithSubscriptions {
           : formatMessage(composition);
     this.attachmentManager.initState({ message });
     this.linkPreviewsManager.initState({ message });
+    this.locationComposer.initState({ message });
     this.textComposer.initState({ message });
     this.pollComposer.initState();
     this.customDataManager.initState({ message });
@@ -403,6 +436,7 @@ export class MessageComposer extends WithSubscriptions {
       this.addUnsubscribeFunction(this.subscribeTextComposerStateChanged());
       this.addUnsubscribeFunction(this.subscribeAttachmentManagerStateChanged());
       this.addUnsubscribeFunction(this.subscribeLinkPreviewsManagerStateChanged());
+      this.addUnsubscribeFunction(this.subscribeLocationComposerStateChanged());
       this.addUnsubscribeFunction(this.subscribePollComposerStateChanged());
       this.addUnsubscribeFunction(this.subscribeCustomDataManagerStateChanged());
       this.addUnsubscribeFunction(this.subscribeMessageComposerStateChanged());
@@ -535,6 +569,18 @@ export class MessageComposer extends WithSubscriptions {
       }
     });
 
+  private subscribeLocationComposerStateChanged = () =>
+    this.locationComposer.state.subscribe((_, previousValue) => {
+      if (typeof previousValue === 'undefined') return;
+
+      this.logStateUpdateTimestamp();
+
+      if (this.compositionIsEmpty) {
+        this.deleteDraft();
+        return;
+      }
+    });
+
   private subscribeLinkPreviewsManagerStateChanged = () =>
     this.linkPreviewsManager.state.subscribe((_, previousValue) => {
       if (typeof previousValue === 'undefined') return;
@@ -800,4 +846,30 @@ export class MessageComposer extends WithSubscriptions {
       throw error;
     }
   };
+
+  sendLocation = async () => {
+    const location = this.locationComposer.validLocation;
+    if (this.threadId || !location) return;
+    try {
+      await this.channel.sendSharedLocation(location);
+      this.refreshId();
+      this.locationComposer.initState();
+    } catch (error) {
+      this.client.notifications.addError({
+        message: 'Failed to share the location',
+        origin: {
+          emitter: 'MessageComposer',
+          context: { composer: this },
+        },
+        options: {
+          type: 'api:location:create:failed',
+          metadata: {
+            reason: (error as Error).message,
+          },
+          originalError: error instanceof Error ? error : undefined,
+        },
+      });
+      throw error;
+    }
+  };
 }
diff --git a/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts b/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts
index d578c1b37..adf06bc91 100644
--- a/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts
+++ b/src/messageComposer/middleware/messageComposer/MessageComposerMiddlewareExecutor.ts
@@ -32,6 +32,7 @@ import {
 } from './customData';
 import { createUserDataInjectionMiddleware } from './userDataInjection';
 import { createPollOnlyCompositionMiddleware } from './pollOnly';
+import { createSharedLocationCompositionMiddleware } from './sharedLocation';
 
 export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<
   MessageComposerMiddlewareState,
@@ -47,6 +48,7 @@ export class MessageComposerMiddlewareExecutor extends MiddlewareExecutor<
       createTextComposerCompositionMiddleware(composer),
       createAttachmentsCompositionMiddleware(composer),
       createLinkPreviewsCompositionMiddleware(composer),
+      createSharedLocationCompositionMiddleware(composer),
       createMessageComposerStateCompositionMiddleware(composer),
       createCustomDataCompositionMiddleware(composer),
       createCompositionValidationMiddleware(composer),
diff --git a/src/messageComposer/middleware/messageComposer/compositionValidation.ts b/src/messageComposer/middleware/messageComposer/compositionValidation.ts
index 90c11d315..d737ce1eb 100644
--- a/src/messageComposer/middleware/messageComposer/compositionValidation.ts
+++ b/src/messageComposer/middleware/messageComposer/compositionValidation.ts
@@ -20,15 +20,11 @@ export const createCompositionValidationMiddleware = (
     }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
       const { maxLengthOnSend } = composer.config.text ?? {};
       const inputText = state.message.text ?? '';
-      const isEmptyMessage =
-        textIsEmpty(inputText) &&
-        !state.message.attachments?.length &&
-        !state.message.poll_id;
 
       const hasExceededMaxLength =
         typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend;
 
-      if (isEmptyMessage || hasExceededMaxLength) {
+      if (composer.compositionIsEmpty || hasExceededMaxLength) {
         return await discard();
       }
 
diff --git a/src/messageComposer/middleware/messageComposer/index.ts b/src/messageComposer/middleware/messageComposer/index.ts
index ef3111674..86ccc475e 100644
--- a/src/messageComposer/middleware/messageComposer/index.ts
+++ b/src/messageComposer/middleware/messageComposer/index.ts
@@ -5,6 +5,7 @@ export * from './compositionValidation';
 export * from './linkPreviews';
 export * from './MessageComposerMiddlewareExecutor';
 export * from './messageComposerState';
+export * from './sharedLocation';
 export * from './textComposer';
 export * from './types';
 export * from './commandInjection';
diff --git a/src/messageComposer/middleware/messageComposer/sharedLocation.ts b/src/messageComposer/middleware/messageComposer/sharedLocation.ts
new file mode 100644
index 000000000..00e17b68d
--- /dev/null
+++ b/src/messageComposer/middleware/messageComposer/sharedLocation.ts
@@ -0,0 +1,42 @@
+import type { MiddlewareHandlerParams } from '../../../middleware';
+import type { MessageComposer } from '../../messageComposer';
+import type {
+  MessageComposerMiddlewareState,
+  MessageCompositionMiddleware,
+} from './types';
+
+export const createSharedLocationCompositionMiddleware = (
+  composer: MessageComposer,
+): MessageCompositionMiddleware => ({
+  id: 'stream-io/message-composer-middleware/shared-location',
+  handlers: {
+    compose: ({
+      state,
+      next,
+      forward,
+    }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
+      const { locationComposer } = composer;
+      const location = locationComposer.validLocation;
+      if (!locationComposer || !location || !composer.client.user) return forward();
+      const timestamp = new Date().toISOString();
+
+      return next({
+        ...state,
+        localMessage: {
+          ...state.localMessage,
+          shared_location: {
+            ...location,
+            channel_cid: composer.channel.cid,
+            created_at: timestamp,
+            updated_at: timestamp,
+            user_id: composer.client.user.id,
+          },
+        },
+        message: {
+          ...state.message,
+          shared_location: location,
+        },
+      });
+    },
+  },
+});
diff --git a/src/types.ts b/src/types.ts
index 764529764..b57fc9de4 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -2287,7 +2287,6 @@ export type Attachment = CustomAttachmentData & {
   original_height?: number;
   original_width?: number;
   pretext?: string;
-  stopped_sharing?: boolean;
   text?: string;
   thumb_url?: string;
   title?: string;
@@ -2348,6 +2347,7 @@ export type ChannelConfigFields = {
   read_events?: boolean;
   replies?: boolean;
   search?: boolean;
+  shared_locations?: boolean;
   typing_events?: boolean;
   uploads?: boolean;
   url_enrichment?: boolean;
@@ -2705,7 +2705,7 @@ export type Logger = (
 export type Message = Partial<
   MessageBase & {
     mentioned_users: string[];
-    shared_location?: SharedLocationRequest;
+    shared_location?: StaticLocationPayload | LiveLocationPayload;
   }
 >;
 
@@ -3966,13 +3966,14 @@ export type DraftMessage = {
   parent_id?: string;
   poll_id?: string;
   quoted_message_id?: string;
+  shared_location?: StaticLocationPayload | LiveLocationPayload; // todo: live-location verify if possible
   show_in_channel?: boolean;
   silent?: boolean;
   type?: MessageLabel;
 };
 
 export type ActiveLiveLocationsAPIResponse = APIResponse & {
-  active_live_locations: SharedLocationResponse[];
+  active_live_locations: SharedLiveLocationResponse[];
 };
 
 export type SharedLocationResponse = {
@@ -3987,11 +3988,51 @@ export type SharedLocationResponse = {
   user_id: string;
 };
 
-export type SharedLocationRequest = {
+export type SharedStaticLocationResponse = {
+  channel_cid: string;
+  created_at: string;
   created_by_device_id: string;
+  latitude: number;
+  longitude: number;
+  message_id: string;
+  updated_at: string;
+  user_id: string;
+};
+
+export type SharedLiveLocationResponse = {
+  channel_cid: string;
+  created_at: string;
+  created_by_device_id: string;
+  end_at: string;
+  latitude: number;
+  longitude: number;
+  message_id: string;
+  updated_at: string;
+  user_id: string;
+};
+
+export type UpdateLocationPayload = {
+  message_id: string;
+  created_by_device_id?: string;
   end_at?: string;
   latitude?: number;
   longitude?: number;
+  user?: { id: string };
+  user_id?: string;
+};
+
+export type StaticLocationPayload = {
+  created_by_device_id: string;
+  latitude: number;
+  longitude: number;
+  message_id: string;
+};
+
+export type LiveLocationPayload = {
+  created_by_device_id: string;
+  end_at: string;
+  latitude: number;
+  longitude: number;
   message_id: string;
 };
 
@@ -4064,9 +4105,9 @@ export type ReminderResponseBase = {
 };
 
 export type ReminderResponse = ReminderResponseBase & {
-  channel: ChannelResponse;
   user: UserResponse;
   message: MessageResponse;
+  channel?: ChannelResponse;
 };
 
 export type ReminderAPIResponse = APIResponse & {
diff --git a/test/unit/LiveLocationManager.test.ts b/test/unit/LiveLocationManager.test.ts
new file mode 100644
index 000000000..114810637
--- /dev/null
+++ b/test/unit/LiveLocationManager.test.ts
@@ -0,0 +1,798 @@
+import { describe, expect, it, vi } from 'vitest';
+import {
+  Coords,
+  LiveLocationManager,
+  LiveLocationManagerConstructorParameters,
+  SharedLiveLocationResponse,
+  StreamChat,
+  UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT,
+  WatchLocationHandler,
+} from '../../src';
+import { getClientWithUser } from './test-utils/getClient';
+import { sleep } from '../../src/utils';
+
+const makeWatchLocation =
+  (
+    coords: Coords[],
+    captureHandler?: (handler: (c: Coords) => void) => void,
+  ): LiveLocationManagerConstructorParameters['watchLocation'] =>
+  (handler) => {
+    if (captureHandler) {
+      captureHandler(handler);
+    } else {
+      coords.forEach((coord) => handler(coord));
+    }
+
+    return () => null;
+  };
+
+describe('LiveLocationManager', () => {
+  const deviceId = 'deviceId';
+  const getDeviceId = vi.fn().mockReturnValue(deviceId);
+  const watchLocation = vi.fn().mockReturnValue(() => null);
+  const user = { id: 'user-id' };
+  const liveLocation: SharedLiveLocationResponse = {
+    channel_cid: 'channel_cid',
+    created_at: 'created_at',
+    created_by_device_id: 'created_by_device_id',
+    end_at: '9999-12-31T23:59:59.535Z',
+    latitude: 1,
+    longitude: 2,
+    message_id: 'liveLocation_message_id',
+    updated_at: 'updated_at',
+    user_id: user.id,
+  };
+  const liveLocation2: SharedLiveLocationResponse = {
+    channel_cid: 'channel_cid2',
+    created_at: 'created_at',
+    created_by_device_id: 'created_by_device_id',
+    end_at: '9999-12-31T23:59:59.535Z',
+    latitude: 1,
+    longitude: 2,
+    message_id: 'liveLocation_message_id2',
+    updated_at: 'updated_at',
+    user_id: user.id,
+  };
+
+  describe('constructor', () => {
+    it('throws if the user is unknown', () => {
+      expect(
+        () =>
+          new LiveLocationManager({
+            client: {} as StreamChat,
+            getDeviceId,
+            watchLocation,
+          }),
+      ).toThrow(expect.any(Error));
+    });
+
+    it('sets up the initial state', async () => {
+      const client = await getClientWithUser({ id: 'user-abc' });
+      const manager = new LiveLocationManager({
+        client,
+        getDeviceId,
+        watchLocation,
+      });
+      expect(manager.deviceId).toEqual(deviceId);
+      expect(manager.getDeviceId).toEqual(getDeviceId);
+      expect(manager.watchLocation).toEqual(watchLocation);
+      expect(manager.state.getLatestValue()).toEqual({
+        messages: new Map(),
+        ready: false,
+      });
+    });
+  });
+
+  describe('live location management', () => {
+    it('retrieves the active live locations and registers subscriptions on init', async () => {
+      const client = await getClientWithUser({ id: 'user-abc' });
+      const getSharedLocationsSpy = vi
+        .spyOn(client, 'getSharedLocations')
+        .mockResolvedValue({ active_live_locations: [], duration: '' });
+      const manager = new LiveLocationManager({
+        client,
+        getDeviceId,
+        watchLocation,
+      });
+
+      expect(getSharedLocationsSpy).toHaveBeenCalledTimes(0);
+      expect(manager.stateIsReady).toBeFalsy();
+      await manager.init();
+      expect(getSharedLocationsSpy).toHaveBeenCalledTimes(1);
+      expect(manager.hasSubscriptions).toBeTruthy();
+      // @ts-expect-error accessing private attribute
+      expect(manager.refCount).toBe(1);
+
+      await manager.init();
+      expect(getSharedLocationsSpy).toHaveBeenCalledTimes(1);
+      expect(manager.hasSubscriptions).toBeTruthy();
+      expect(manager.stateIsReady).toBeTruthy();
+      // @ts-expect-error accessing private attribute
+      expect(manager.refCount).toBe(2);
+    });
+
+    it('unregisters subscriptions', async () => {
+      const client = await getClientWithUser({ id: 'user-abc' });
+      const getSharedLocationsSpy = vi
+        .spyOn(client, 'getSharedLocations')
+        .mockResolvedValue({ active_live_locations: [], duration: '' });
+      const manager = new LiveLocationManager({
+        client,
+        getDeviceId,
+        watchLocation,
+      });
+
+      await manager.init();
+      manager.unregisterSubscriptions();
+      expect(manager.hasSubscriptions).toBeFalsy();
+    });
+
+    describe('message addition or removal', () => {
+      it('does not update active location if there are no active live locations', async () => {
+        const client = await getClientWithUser({ id: 'user-abc' });
+        const getSharedLocationsSpy = vi
+          .spyOn(client, 'getSharedLocations')
+          .mockResolvedValue({ active_live_locations: [], duration: '' });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(updateLocationSpy).not.toHaveBeenCalled();
+      });
+
+      it('does not update active location if there are no coordinate updates', async () => {
+        // starting from 0
+        const client = await getClientWithUser({ id: 'user-abc' });
+        const getSharedLocationsSpy = vi
+          .spyOn(client, 'getSharedLocations')
+          .mockResolvedValue({ active_live_locations: [liveLocation], duration: '' });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValue(liveLocation);
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation,
+        });
+
+        await manager.init();
+        expect(updateLocationSpy).not.toHaveBeenCalled();
+      });
+
+      it('updates active location on coordinate updates', async () => {
+        const client = await getClientWithUser({ id: 'user-abc' });
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [liveLocation],
+          duration: '',
+        });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+        expect(updateLocationSpy).toHaveBeenCalledWith({
+          created_by_device_id: liveLocation.created_by_device_id,
+          message_id: liveLocation.message_id,
+          ...newCoords,
+        });
+        expect(manager.messages).toHaveLength(1);
+      });
+
+      it('does not update active location if returning to 0 locations', async () => {
+        const client = await getClientWithUser({ id: 'user-abc' });
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [liveLocation],
+          duration: '',
+        });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+
+        // @ts-expect-error accessing private property
+        manager.unregisterMessages([liveLocation.message_id]);
+        expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+        expect(manager.messages).toHaveLength(0);
+      });
+
+      it('requests the live location upon adding a first message', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [],
+          duration: '',
+        });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(updateLocationSpy).not.toHaveBeenCalled();
+        // @ts-expect-error accessing private property
+        manager.registerMessage({
+          id: liveLocation.message_id,
+          shared_location: liveLocation,
+          user,
+        });
+        vi.waitFor(() => {
+          expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+          expect(updateLocationSpy).toHaveBeenCalledWith({
+            created_by_device_id: manager.deviceId,
+            message_id: liveLocation.message_id,
+            ...newCoords,
+          });
+          expect(manager.messages).toHaveLength(1);
+        });
+      });
+
+      it('does not perform live location update request upon adding subsequent messages within min throttle timeout', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [],
+          duration: '',
+        });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        // @ts-expect-error accessing private property
+        manager.registerMessage({
+          id: liveLocation.message_id,
+          shared_location: liveLocation,
+          user,
+        });
+        await sleep(0); // registerMessage is async under the hood
+        // @ts-expect-error accessing private property
+        manager.registerMessage({
+          id: liveLocation2.message_id,
+          shared_location: liveLocation2,
+          user,
+        });
+
+        vi.waitFor(() => {
+          expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+          expect(updateLocationSpy).toHaveBeenCalledWith({
+            created_by_device_id: manager.deviceId,
+            message_id: liveLocation.message_id,
+            ...newCoords,
+          });
+          expect(manager.messages).toHaveLength(2);
+        });
+      });
+
+      it('does not request live location upon adding subsequent messages beyond min throttle timeout', async () => {
+        vi.useFakeTimers();
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [],
+          duration: '',
+        });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValueOnce(liveLocation)
+          .mockResolvedValueOnce(liveLocation2);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        // @ts-expect-error accessing private property
+        manager.registerMessage({
+          id: liveLocation.message_id,
+          shared_location: liveLocation,
+          user,
+        });
+        let sleepPromise = sleep(0); // registerMessage is async under the hood
+        vi.advanceTimersByTime(UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT);
+        await sleepPromise;
+        // @ts-expect-error accessing private property
+        manager.registerMessage({
+          id: liveLocation2.message_id,
+          shared_location: liveLocation2,
+          user,
+        });
+        sleepPromise = sleep(0); // registerMessage is async under the hood
+        vi.advanceTimersByTime(0);
+        await sleepPromise;
+
+        vi.waitFor(() => {
+          expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+          expect(updateLocationSpy).toHaveBeenCalledWith({
+            created_by_device_id: liveLocation.created_by_device_id,
+            message_id: liveLocation.message_id,
+            ...newCoords,
+          });
+          expect(manager.messages).toHaveLength(2);
+        });
+        vi.useRealTimers();
+      });
+
+      it('throttles live location update requests upon multiple watcher coords emissions under min throttle timeout', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [liveLocation],
+          duration: '',
+        });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValue(liveLocation);
+        let watchHandler: WatchLocationHandler = () => {
+          throw new Error('XX');
+        };
+        const captureHandler = (handler: WatchLocationHandler) => {
+          watchHandler = handler;
+        };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([], captureHandler),
+        });
+
+        await manager.init();
+
+        watchHandler({ latitude: 1, longitude: 1 });
+
+        await sleep(0); // async under the hood
+        expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+
+        watchHandler({ latitude: 1, longitude: 2 });
+
+        await sleep(0); // async under the hood
+        expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+      });
+
+      it('allows live location update requests upon multiple watcher coords emissions beyond min throttle timeout', async () => {
+        vi.useFakeTimers();
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [liveLocation],
+          duration: '',
+        });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValue(liveLocation);
+        let watchHandler: WatchLocationHandler = () => {
+          throw new Error('XX');
+        };
+        const captureHandler = (handler: WatchLocationHandler) => {
+          watchHandler = handler;
+        };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([], captureHandler),
+        });
+
+        await manager.init();
+        watchHandler({ latitude: 1, longitude: 1 });
+
+        vi.waitFor(() => {
+          expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+        });
+
+        const sleepPromise = sleep(0);
+        vi.advanceTimersByTime(UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT);
+        await sleepPromise;
+
+        watchHandler({ latitude: 3, longitude: 4 });
+
+        vi.waitFor(() => {
+          expect(updateLocationSpy).toHaveBeenCalledTimes(2);
+        });
+
+        vi.useRealTimers();
+      });
+
+      it('prevents live location update requests for expired live locations', async () => {
+        vi.useFakeTimers();
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [
+            {
+              ...liveLocation,
+              end_at: new Date(
+                Date.now() + UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT - 1000,
+              ).toISOString(),
+            },
+          ],
+          duration: '',
+        });
+        const updateLocationSpy = vi
+          .spyOn(client, 'updateLocation')
+          .mockResolvedValue(liveLocation);
+        let watchHandler: WatchLocationHandler = () => {
+          throw new Error('XX');
+        };
+        const captureHandler = (handler: WatchLocationHandler) => {
+          watchHandler = handler;
+        };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([], captureHandler),
+        });
+
+        await manager.init();
+        watchHandler({ latitude: 1, longitude: 1 });
+
+        vi.waitFor(() => {
+          expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+        });
+
+        const sleepPromise = sleep(0);
+        vi.advanceTimersByTime(UPDATE_LIVE_LOCATION_REQUEST_MIN_THROTTLE_TIMEOUT);
+        await sleepPromise;
+
+        watchHandler({ latitude: 3, longitude: 4 });
+
+        vi.waitFor(() => {
+          expect(updateLocationSpy).toHaveBeenCalledTimes(1);
+        });
+
+        vi.useRealTimers();
+      });
+    });
+
+    describe('live_location_sharing.started', () => {
+      it('registers a new message', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(manager.messages.size).toBe(0);
+        client.dispatchEvent({
+          message: {
+            id: liveLocation.message_id,
+            shared_location: liveLocation,
+            type: 'regular',
+            user,
+          },
+          type: 'live_location_sharing.started',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages.size).toBe(1);
+        });
+      });
+    });
+
+    describe('message.updated', () => {
+      it('registers a new message if not yet registered', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(manager.messages.size).toBe(0);
+        client.dispatchEvent({
+          message: {
+            id: liveLocation.message_id,
+            shared_location: liveLocation,
+            type: 'regular',
+            user,
+          },
+          type: 'message.updated',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages.size).toBe(1);
+        });
+      });
+
+      it('updates location for registered message', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [{ ...liveLocation, end_at: new Date().toISOString() }],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        vi.waitFor(() => {
+          expect(manager.messages).toHaveLength(1);
+        });
+        client.dispatchEvent({
+          message: {
+            id: liveLocation.message_id,
+            shared_location: liveLocation,
+            type: 'regular',
+            user,
+          },
+          type: 'message.updated',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages).toHaveLength(1);
+          expect(manager.messages.get(liveLocation.message_id)?.end_at).toBe(
+            liveLocation.end_at,
+          );
+        });
+      });
+
+      it('does not register a new message if it does not contain a live location', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(manager.messages.size).toBe(0);
+        client.dispatchEvent({
+          message: { id: liveLocation.message_id, type: 'regular', user },
+          type: 'message.updated',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages.size).toBe(0);
+        });
+      });
+
+      it('does not register a new message if it does not contain user', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(manager.messages.size).toBe(0);
+        client.dispatchEvent({
+          message: {
+            id: liveLocation.message_id,
+            shared_location: liveLocation,
+            type: 'regular',
+          },
+          type: 'message.updated',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages.size).toBe(0);
+        });
+      });
+
+      it('unregisters a message if the updated message does not contain a live location', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [liveLocation],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(manager.messages).toHaveLength(1);
+        client.dispatchEvent({
+          message: {
+            id: liveLocation.message_id,
+            shared_location: undefined,
+            type: 'regular',
+            user,
+          },
+          type: 'message.updated',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages).toHaveLength(0);
+        });
+      });
+
+      it('unregisters a message if its live location has been changed to static location', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [liveLocation],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(manager.messages).toHaveLength(1);
+        const newEndAt = '1970-01-01T08:08:08.532Z';
+        client.dispatchEvent({
+          message: {
+            id: liveLocation.message_id,
+            shared_location: { ...liveLocation, end_at: undefined },
+            type: 'regular',
+            user,
+          },
+          type: 'message.updated',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages).toHaveLength(0);
+        });
+      });
+
+      it('unregisters a message if the updated message has end_at in the past', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [liveLocation],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(manager.messages).toHaveLength(1);
+        const newEndAt = '1970-01-01T08:08:08.532Z';
+        client.dispatchEvent({
+          message: {
+            id: liveLocation.message_id,
+            shared_location: { ...liveLocation, end_at: newEndAt },
+            type: 'regular',
+            user,
+          },
+          type: 'message.updated',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages).toHaveLength(0);
+        });
+      });
+    });
+
+    describe('live_location_sharing.stopped', () => {
+      it('unregisters a message', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [liveLocation],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(manager.messages).toHaveLength(1);
+        client.dispatchEvent({
+          live_location: liveLocation,
+          type: 'live_location_sharing.stopped',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages).toHaveLength(0);
+        });
+      });
+    });
+
+    describe('message.deleted', () => {
+      it('unregisters a message', async () => {
+        const client = await getClientWithUser(user);
+        vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+          active_live_locations: [liveLocation],
+          duration: '',
+        });
+        vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+        const newCoords = { latitude: 2, longitude: 2 };
+        const manager = new LiveLocationManager({
+          client,
+          getDeviceId,
+          watchLocation: makeWatchLocation([newCoords]),
+        });
+
+        await manager.init();
+        expect(manager.messages).toHaveLength(1);
+        client.dispatchEvent({
+          message: {
+            id: liveLocation.message_id,
+            shared_location: liveLocation,
+            type: 'regular',
+            user,
+          },
+          type: 'message.deleted',
+        });
+        vi.waitFor(() => {
+          expect(manager.messages).toHaveLength(0);
+        });
+      });
+    });
+  });
+
+  describe('getters', async () => {
+    it('deviceId is calculated only once', async () => {
+      const client = await getClientWithUser(user);
+      vi.spyOn(client, 'getSharedLocations').mockResolvedValue({
+        active_live_locations: [liveLocation],
+        duration: '',
+      });
+      vi.spyOn(client, 'updateLocation').mockResolvedValue(liveLocation);
+      const getDeviceId = vi
+        .fn()
+        .mockReturnValueOnce(deviceId)
+        .mockReturnValueOnce('xxx');
+      const manager = new LiveLocationManager({
+        client,
+        getDeviceId,
+        watchLocation,
+      });
+      expect(manager.deviceId).toBe(deviceId);
+      expect(manager.deviceId).toBe(deviceId);
+    });
+  });
+});
diff --git a/test/unit/MessageComposer/LocationComposer.test.ts b/test/unit/MessageComposer/LocationComposer.test.ts
new file mode 100644
index 000000000..e86267ed7
--- /dev/null
+++ b/test/unit/MessageComposer/LocationComposer.test.ts
@@ -0,0 +1,209 @@
+import { describe, expect, it, vi } from 'vitest';
+import {
+  DraftResponse,
+  LocalMessage,
+  LocationComposerConfig,
+  MessageComposer,
+  StreamChat,
+} from '../../../src';
+
+const deviceId = 'deviceId';
+
+const defaultConfig: LocationComposerConfig = {
+  enabled: true,
+  getDeviceId: () => deviceId,
+};
+
+const user = { id: 'user-id' };
+
+const setup = ({
+  composition,
+  config,
+}: {
+  composition?: DraftResponse | LocalMessage;
+  config?: Partial<LocationComposerConfig>;
+} = {}) => {
+  // Reset mocks
+  vi.clearAllMocks();
+
+  // Setup mocks
+  const mockClient = new StreamChat('apiKey', 'apiSecret');
+  mockClient.user = user;
+
+  const mockChannel = mockClient.channel('channelType', 'channelId');
+  mockChannel.getClient = vi.fn().mockReturnValue(mockClient);
+  const messageComposer = new MessageComposer({
+    client: mockClient,
+    composition,
+    compositionContext: mockChannel,
+    config: { location: { ...defaultConfig, ...config } },
+  });
+  return { mockClient, mockChannel, messageComposer };
+};
+const locationMessage: LocalMessage = {
+  created_at: new Date(),
+  updated_at: new Date(),
+  deleted_at: null,
+  pinned_at: null,
+  type: 'regular',
+  status: 'received',
+  id: 'messageId',
+  shared_location: {
+    channel_cid: 'channel_cid',
+    created_at: 'created_at',
+    created_by_device_id: 'created_by_device_id',
+    end_at: '9999-12-31T23:59:59.535Z',
+    latitude: 1,
+    longitude: 2,
+    message_id: 'liveLocation_message_id',
+    updated_at: 'updated_at',
+    user_id: user.id,
+  },
+};
+describe('LocationComposer', () => {
+  it('constructor initiates state and variables', () => {
+    const {
+      messageComposer: { locationComposer },
+    } = setup();
+    expect(locationComposer.state.getLatestValue()).toEqual({
+      location: null,
+    });
+    expect(locationComposer.deviceId).toBe(deviceId);
+    expect(locationComposer.config).toEqual(defaultConfig);
+  });
+
+  it('overrides state with initState', () => {
+    const {
+      messageComposer: { locationComposer },
+    } = setup();
+    locationComposer.initState({ message: locationMessage });
+    expect(locationComposer.state.getLatestValue()).toEqual({
+      location: locationMessage.shared_location,
+    });
+  });
+
+  it('does not override state with initState with message without shared_location', () => {
+    const {
+      messageComposer: { locationComposer },
+    } = setup();
+    locationComposer.initState({
+      message: { ...locationMessage, shared_location: undefined },
+    });
+    expect(locationComposer.state.getLatestValue()).toEqual({
+      location: null,
+    });
+  });
+
+  it('does not override state with initState without message', () => {
+    const {
+      messageComposer: { locationComposer },
+    } = setup();
+    locationComposer.initState();
+    expect(locationComposer.state.getLatestValue()).toEqual({
+      location: null,
+    });
+  });
+
+  it('sets the data', () => {
+    const {
+      messageComposer: { locationComposer },
+    } = setup();
+    const data = {
+      durationMs: 1,
+      latitude: 2,
+      longitude: 3,
+    };
+    locationComposer.setData(data);
+    const messageId = locationComposer.composer.id;
+    expect(locationComposer.location).toEqual({
+      message_id: messageId,
+      created_by_device_id: deviceId,
+      ...data,
+    });
+  });
+
+  it('does not set the data in case latitude or longitude is missing', () => {
+    const {
+      messageComposer: { locationComposer },
+    } = setup();
+    locationComposer.setData({});
+    expect(locationComposer.location).toBeNull();
+  });
+
+  it('does not generate location payload for send message request if expires in less than 60 seconds', () => {
+    const {
+      messageComposer: { locationComposer },
+    } = setup();
+    const data = {
+      durationMs: 59 * 1000,
+      latitude: 2,
+      longitude: 3,
+    };
+    locationComposer.setData(data);
+    expect(locationComposer.validLocation).toEqual(null);
+  });
+
+  it('generate location payload for send message request', () => {
+    const {
+      messageComposer: { locationComposer },
+    } = setup();
+    const data = {
+      durationMs: 60 * 1000,
+      latitude: 2,
+      longitude: 3,
+    };
+    const messageId = locationComposer.composer.id;
+    locationComposer.setData(data);
+    expect(locationComposer.validLocation).toEqual({
+      message_id: messageId,
+      created_by_device_id: deviceId,
+      latitude: data.latitude,
+      longitude: data.longitude,
+      end_at: expect.any(String),
+    });
+
+    const endAt = new Date(locationComposer.validLocation!.end_at);
+    const expectedEndAt = new Date(Date.now() + data.durationMs);
+    expect(endAt.getTime()).toBeCloseTo(expectedEndAt.getTime(), -2); // Within 100ms
+  });
+
+  it('generates null in case of invalid location state', () => {
+    const {
+      messageComposer: { locationComposer },
+    } = setup();
+    const invalidStates = [
+      {
+        location: {
+          latitude: 1,
+          created_by_device_id: deviceId,
+          message_id: locationComposer.composer.id,
+        },
+      },
+      {
+        location: {
+          longitude: 1,
+          created_by_device_id: deviceId,
+          message_id: locationComposer.composer.id,
+        },
+      },
+      {
+        location: {
+          latitude: 1,
+          longitude: 1,
+          message_id: locationComposer.composer.id,
+        },
+      },
+      {
+        location: {
+          latitude: 1,
+          longitude: 1,
+          created_by_device_id: deviceId,
+        },
+      },
+    ];
+    invalidStates.forEach((state) => {
+      locationComposer.state.next(state);
+      expect(locationComposer.validLocation).toBeNull();
+    });
+  });
+});
diff --git a/test/unit/MessageComposer/messageComposer.test.ts b/test/unit/MessageComposer/messageComposer.test.ts
index cda63a6a4..a5f79559b 100644
--- a/test/unit/MessageComposer/messageComposer.test.ts
+++ b/test/unit/MessageComposer/messageComposer.test.ts
@@ -1,16 +1,16 @@
-import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import {
   AbstractOfflineDB,
   Channel,
   ChannelAPIResponse,
   LocalMessage,
   MessageComposerConfig,
+  StaticLocationPayload,
   StreamChat,
   Thread,
 } from '../../../src';
 import { DeepPartial } from '../../../src/types.utility';
 import { MessageComposer } from '../../../src/messageComposer/messageComposer';
-import { StateStore } from '../../../src/store';
 import { DraftResponse, MessageResponse } from '../../../src/types';
 import { MockOfflineDB } from '../offline-support/MockOfflineDB';
 
@@ -32,24 +32,6 @@ vi.mock('../../../src/utils', () => ({
   throttle: vi.fn().mockImplementation((fn) => fn),
 }));
 
-vi.mock('../../../src/messageComposer/attachmentManager', () => ({
-  AttachmentManager: vi.fn().mockImplementation(() => ({
-    state: new StateStore({ attachments: [] }),
-    initState: vi.fn(),
-    clear: vi.fn(),
-    attachments: [],
-  })),
-}));
-
-vi.mock('../../../src/messageComposer/pollComposer', () => ({
-  PollComposer: vi.fn().mockImplementation(() => ({
-    state: new StateStore({ poll: null }),
-    initState: vi.fn(),
-    clear: vi.fn(),
-    compose: vi.fn(),
-  })),
-}));
-
 vi.mock('../../../src/messageComposer/middleware/messageComposer', () => ({
   MessageComposerMiddlewareExecutor: vi.fn().mockImplementation(() => ({
     execute: vi.fn().mockResolvedValue({ state: {} }),
@@ -106,15 +88,25 @@ const getThread = (channel: Channel, client: StreamChat, threadId: string) =>
 const setup = ({
   composition,
   compositionContext,
+  channelConfig,
   config,
 }: {
   composition?: LocalMessage | DraftResponse | MessageResponse | undefined;
   compositionContext?: Channel | Thread | LocalMessage | undefined;
+  channelConfig?: {
+    polls?: boolean;
+    shared_locations?: boolean;
+  };
   config?: DeepPartial<MessageComposerConfig>;
 } = {}) => {
   const mockClient = new StreamChat('test-api-key');
   mockClient.user = user;
   mockClient.userID = user.id;
+  const cid = 'messaging:test-channel-id';
+  if (channelConfig) {
+    // @ts-expect-error incomplete channel config object
+    mockClient.configs[cid] = channelConfig;
+  }
   // Create a proper Channel instance with only the necessary attributes mocked
   const mockChannel = new Channel(mockClient, 'messaging', 'test-channel-id', {
     id: 'test-channel-id',
@@ -201,6 +193,61 @@ describe('MessageComposer', () => {
       expect(messageComposer.config.text?.maxLengthOnEdit).toBe(1000);
     });
 
+    it('should initialize with custom config overridden with back-end configuration', () => {
+      [
+        {
+          customConfig: { location: { enabled: true } },
+          channelConfig: { shared_locations: undefined },
+          expectedResult: { location: { enabled: true } }, // default is true
+        },
+        {
+          customConfig: { location: { enabled: true } },
+          channelConfig: { shared_locations: false },
+          expectedResult: { location: { enabled: false } },
+        },
+        {
+          customConfig: { location: { enabled: true } },
+          channelConfig: { shared_locations: true },
+          expectedResult: { location: { enabled: true } },
+        },
+        {
+          customConfig: { location: { enabled: undefined } },
+          channelConfig: { shared_locations: undefined },
+          expectedResult: { location: { enabled: true } }, // default is true
+        },
+        {
+          customConfig: { location: { enabled: undefined } },
+          channelConfig: { shared_locations: false },
+          expectedResult: { location: { enabled: false } },
+        },
+        {
+          customConfig: { location: { enabled: undefined } },
+          channelConfig: { shared_locations: true },
+          expectedResult: { location: { enabled: true } },
+        },
+        {
+          customConfig: { location: { enabled: false } },
+          channelConfig: { shared_locations: false },
+          expectedResult: { location: { enabled: false } },
+        },
+        {
+          customConfig: { location: { enabled: false } },
+          channelConfig: { shared_locations: undefined },
+          expectedResult: { location: { enabled: false } },
+        },
+        {
+          customConfig: { location: { enabled: false } },
+          channelConfig: { shared_locations: true },
+          expectedResult: { location: { enabled: false } },
+        },
+      ].forEach(({ customConfig, channelConfig, expectedResult }) => {
+        const { messageComposer } = setup({ channelConfig, config: customConfig });
+        expect(messageComposer.config.location.enabled).toBe(
+          expectedResult.location.enabled,
+        );
+      });
+    });
+
     it('should initialize with message', () => {
       const message = {
         id: 'test-message-id',
@@ -468,28 +515,115 @@ describe('MessageComposer', () => {
       expect(messageComposer.lastChangeOriginIsLocal).toBe(true);
     });
 
-    it('should return the correct compositionIsEmpty', () => {
+    it('should return the correct hasSendableData', () => {
       const { messageComposer } = setup();
-      const spyTextComposerTextIsEmpty = vi
-        .spyOn(messageComposer.textComposer, 'textIsEmpty', 'get')
-        .mockReturnValueOnce(true)
-        .mockReturnValueOnce(false);
-      // First case - empty composition
+
       messageComposer.textComposer.state.partialNext({
         text: '',
         mentionedUsers: [],
         selection: { start: 0, end: 0 },
       });
-      expect(messageComposer.compositionIsEmpty).toBe(true);
+      expect(messageComposer.hasSendableData).toBe(false);
 
-      // Second case - non-empty composition
       messageComposer.textComposer.state.partialNext({
         text: 'Hello world',
+      });
+      expect(messageComposer.hasSendableData).toBe(true);
+      messageComposer.textComposer.state.partialNext({
+        text: '',
+      });
+
+      messageComposer.setQuotedMessage({
+        id: 'id',
+        type: 'regular',
+        status: 'delivered',
+        created_at: new Date(),
+        updated_at: new Date(),
+        deleted_at: null,
+        pinned_at: null,
+      });
+      expect(messageComposer.hasSendableData).toBe(false);
+      messageComposer.setQuotedMessage(null);
+
+      messageComposer.attachmentManager.state.partialNext({
+        attachments: [
+          { type: 'x', localMetadata: { id: 'x,', uploadState: 'finished', file: {} } },
+        ],
+      });
+      expect(messageComposer.hasSendableData).toBe(true);
+      messageComposer.attachmentManager.state.partialNext({
+        attachments: [
+          { type: 'x', localMetadata: { id: 'x,', uploadState: 'finished', file: {} } },
+          { type: 'x', localMetadata: { id: 'x,', uploadState: 'uploading', file: {} } },
+        ],
+      });
+      expect(messageComposer.hasSendableData).toBe(false);
+      messageComposer.attachmentManager.state.partialNext({
+        attachments: [],
+      });
+
+      messageComposer.state.partialNext({ pollId: 'pollId' });
+      expect(messageComposer.hasSendableData).toBe(true);
+      messageComposer.state.partialNext({ pollId: null });
+
+      messageComposer.updateConfig({ location: { enabled: true } });
+      messageComposer.locationComposer.setData({ latitude: 1, longitude: 1 });
+      expect(messageComposer.hasSendableData).toBe(true);
+      messageComposer.locationComposer.initState();
+
+      expect(messageComposer.hasSendableData).toBe(false);
+    });
+
+    it('should return the correct compositionIsEmpty', () => {
+      const { messageComposer } = setup();
+
+      messageComposer.textComposer.state.partialNext({
+        text: '',
         mentionedUsers: [],
         selection: { start: 0, end: 0 },
       });
+      expect(messageComposer.compositionIsEmpty).toBe(true);
+
+      messageComposer.textComposer.state.partialNext({
+        text: 'Hello world',
+      });
+      expect(messageComposer.compositionIsEmpty).toBe(false);
+      messageComposer.textComposer.state.partialNext({
+        text: '',
+      });
+
+      messageComposer.setQuotedMessage({
+        id: 'id',
+        type: 'regular',
+        status: 'delivered',
+        created_at: new Date(),
+        updated_at: new Date(),
+        deleted_at: null,
+        pinned_at: null,
+      });
       expect(messageComposer.compositionIsEmpty).toBe(false);
-      spyTextComposerTextIsEmpty.mockRestore();
+      messageComposer.setQuotedMessage(null);
+
+      messageComposer.attachmentManager.state.partialNext({
+        attachments: [
+          { type: 'x', localMetadata: { id: 'x,', uploadState: 'finished', file: {} } },
+        ],
+      });
+      expect(messageComposer.compositionIsEmpty).toBe(false);
+      messageComposer.attachmentManager.state.partialNext({
+        attachments: [],
+      });
+
+      messageComposer.state.partialNext({ pollId: 'pollId' });
+      expect(messageComposer.compositionIsEmpty).toBe(false);
+      messageComposer.state.partialNext({ pollId: null });
+
+      messageComposer.updateConfig({ location: { enabled: true } });
+      messageComposer.locationComposer.setData({ latitude: 1, longitude: 1 });
+      expect(messageComposer.compositionIsEmpty).toBe(false);
+      messageComposer.locationComposer.initState();
+
+      expect(messageComposer.compositionIsEmpty).toBe(true);
     });
   });
 
@@ -687,6 +821,7 @@ describe('MessageComposer', () => {
       );
       const spyTextComposer = vi.spyOn(messageComposer.textComposer, 'initState');
       const spyPollComposer = vi.spyOn(messageComposer.pollComposer, 'initState');
+      const spyLocationComposer = vi.spyOn(messageComposer.locationComposer, 'initState');
       const spyCustomDataManager = vi.spyOn(
         messageComposer.customDataManager,
         'initState',
@@ -701,6 +836,7 @@ describe('MessageComposer', () => {
       expect(spyPollComposer).toHaveBeenCalled();
       expect(spyCustomDataManager).toHaveBeenCalled();
       expect(spyInitState).toHaveBeenCalled();
+      expect(spyLocationComposer).toHaveBeenCalled();
       expect(messageComposer.quotedMessage).to.be.null;
     });
 
@@ -1139,6 +1275,10 @@ describe('MessageComposer', () => {
 
       const spyCompose = vi.spyOn(messageComposer.pollComposer, 'compose');
       spyCompose.mockResolvedValue({ data: mockPoll });
+      const spyPollComposerInitState = vi.spyOn(
+        messageComposer.pollComposer,
+        'initState',
+      );
 
       const spyCreatePoll = vi.spyOn(mockClient, 'createPoll');
       spyCreatePoll.mockResolvedValue({ poll: mockPoll });
@@ -1147,7 +1287,7 @@ describe('MessageComposer', () => {
 
       expect(spyCompose).toHaveBeenCalled();
       expect(spyCreatePoll).toHaveBeenCalledWith(mockPoll);
-      expect(messageComposer.pollComposer.initState).not.toHaveBeenCalled();
+      expect(spyPollComposerInitState).not.toHaveBeenCalled();
       expect(messageComposer.state.getLatestValue().pollId).toBe('test-poll-id');
     });
 
@@ -1197,6 +1337,91 @@ describe('MessageComposer', () => {
         },
       });
     });
+
+    it('sends location message', async () => {
+      const { messageComposer, mockChannel } = setup();
+      messageComposer.locationComposer.setData({ latitude: 1, longitude: 1 });
+      const messageId = messageComposer.id;
+      const spySendSharedLocation = vi
+        .spyOn(mockChannel, 'sendSharedLocation')
+        .mockResolvedValue({
+          message: { id: 'x', status: 'received', type: 'regular' },
+          duration: '',
+        });
+
+      await messageComposer.sendLocation();
+
+      expect(spySendSharedLocation).toHaveBeenCalled();
+      expect(spySendSharedLocation).toHaveBeenCalledWith({
+        message_id: messageId,
+        created_by_device_id: messageComposer.locationComposer.deviceId,
+        latitude: 1,
+        longitude: 1,
+      } as StaticLocationPayload);
+      expect(messageComposer.locationComposer.state.getLatestValue()).toEqual({
+        location: null,
+      });
+    });
+
+    it('prevents sending location message when location data is invalid', async () => {
+      const { messageComposer, mockChannel } = setup();
+      const spySendSharedLocation = vi
+        .spyOn(mockChannel, 'sendSharedLocation')
+        .mockResolvedValue({
+          message: { id: 'x', status: 'received', type: 'regular' },
+          duration: '',
+        });
+
+      await messageComposer.sendLocation();
+
+      expect(spySendSharedLocation).not.toHaveBeenCalled();
+    });
+    it('prevents sending location message in thread', async () => {
+      const { mockChannel, mockClient } = setup();
+      const mockThread = getThread(mockChannel, mockClient, 'test-thread-id');
+      const { messageComposer: threadComposer } = setup({
+        compositionContext: mockThread,
+      });
+      threadComposer.locationComposer.setData({ latitude: 1, longitude: 1 });
+      const spySendSharedLocation = vi
+        .spyOn(mockChannel, 'sendSharedLocation')
+        .mockResolvedValue({
+          message: { id: 'x', status: 'received', type: 'regular' },
+          duration: '',
+        });
+
+      await threadComposer.sendLocation();
+
+      expect(spySendSharedLocation).not.toHaveBeenCalled();
+    });
+
+    it('handles failed location message request', async () => {
+      const { messageComposer, mockChannel, mockClient } = setup();
+      const error = new Error('Failed location request');
+      messageComposer.locationComposer.setData({ latitude: 1, longitude: 1 });
+      const messageId = messageComposer.id;
+      const spySendSharedLocation = vi
+        .spyOn(mockChannel, 'sendSharedLocation')
+        .mockRejectedValue(error);
+      const spyAddNotification = vi.spyOn(mockClient.notifications, 'add');
+
+      await expect(messageComposer.sendLocation()).rejects.toThrow(error.message);
+      expect(spyAddNotification).toHaveBeenCalledWith({
+        message: 'Failed to share the location',
+        origin: {
+          emitter: 'MessageComposer',
+          context: { composer: messageComposer },
+        },
+        options: {
+          type: 'api:location:create:failed',
+          metadata: {
+            reason: error.message,
+          },
+          originalError: expect.any(Error),
+          severity: 'error',
+        },
+      });
+    });
   });
 
   describe('getDraft', () => {
@@ -1728,6 +1953,37 @@ describe('MessageComposer', () => {
       });
     });
 
+    describe('subscribeLocationComposerStateChanged', () => {
+      it('should log state update timestamp when attachments change', () => {
+        const { messageComposer } = setup();
+        const spy = vi.spyOn(messageComposer, 'logStateUpdateTimestamp');
+        const spyDeleteDraft = vi.spyOn(messageComposer, 'deleteDraft');
+
+        messageComposer.registerSubscriptions();
+        messageComposer.locationComposer.setData({
+          latitude: 1,
+          longitude: 1,
+        });
+
+        expect(spy).toHaveBeenCalled();
+        expect(spyDeleteDraft).not.toHaveBeenCalled();
+      });
+      it('deletes the draft when composition becomes empty', () => {
+        const { messageComposer } = setup();
+        vi.spyOn(messageComposer, 'logStateUpdateTimestamp');
+        const spyDeleteDraft = vi.spyOn(messageComposer, 'deleteDraft');
+        messageComposer.registerSubscriptions();
+        messageComposer.locationComposer.setData({
+          latitude: 1,
+          longitude: 1,
+        });
+
+        messageComposer.locationComposer.state.next({ location: null });
+
+        expect(spyDeleteDraft).toHaveBeenCalled();
+      });
+    });
+
     it('should toggle the registration of draft WS event subscriptions when drafts are disabled / enabled', () => {
       const { messageComposer } = setup({
         config: { drafts: { enabled: false } },
diff --git a/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts b/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts
index ba89ac9c4..0a881867e 100644
--- a/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts
+++ b/test/unit/MessageComposer/middleware/messageComposer/compositionValidation.test.ts
@@ -13,91 +13,31 @@ import {
 import { MiddlewareStatus } from '../../../../../src/middleware';
 import { MessageComposerMiddlewareState } from '../../../../../src/messageComposer/middleware/messageComposer/types';
 import { MessageDraftComposerMiddlewareValueState } from '../../../../../src/messageComposer/middleware/messageComposer/types';
-import { LocalMessage } from '../../../../../src';
-
-const setupMiddleware = (custom: { composer?: MessageComposer } = {}) => {
-  const client = {
-    userID: 'currentUser',
-    user: { id: 'currentUser' },
-  } as any;
-
-  const channel = {
-    getClient: vi.fn().mockReturnValue(client),
-    state: {
-      members: {},
-      watchers: {},
-    },
-    getConfig: vi.fn().mockReturnValue({ commands: [] }),
-  } as any;
-
-  const textComposer = {
-    get text() {
-      return '';
-    },
-    get mentionedUsers() {
-      return [];
-    },
-  };
-
-  const attachmentManager = {
-    get uploadsInProgressCount() {
-      return 0;
-    },
-    get successfulUploads() {
-      return [];
-    },
-  };
-
-  const linkPreviewsManager = {
-    state: {
-      getLatestValue: () => ({
-        previews: new Map(),
-      }),
-    },
-  };
+import { LocalMessage, MessageResponse } from '../../../../../src';
+import { generateChannel } from '../../../test-utils/generateChannel';
 
-  const pollComposer = {
-    state: {
-      getLatestValue: () => ({
-        data: {
-          options: [],
-          name: '',
-          max_votes_allowed: '',
-          id: '',
-          user_id: '',
-          voting_visibility: 'public',
-          allow_answers: false,
-          allow_user_suggested_options: false,
-          description: '',
-          enforce_unique_vote: true,
-        },
-        errors: {},
-      }),
-    },
-    get canCreatePoll() {
-      return false;
-    },
-  };
+const setupMiddleware = (
+  custom: { composer?: MessageComposer; editedMessage?: MessageResponse } = {},
+) => {
+  const user = { id: 'user' };
+  const client = new StreamChat('apiKey');
+  client.user = user;
+  client.userID = user.id;
+
+  const channelResponse = generateChannel();
+  const channel = client.channel(
+    channelResponse.channel.type,
+    channelResponse.channel.id,
+  );
+  channel.initialized = true;
 
   const messageComposer =
     custom.composer ??
-    ({
-      channel,
-      config: {},
-      threadId: undefined,
+    new MessageComposer({
       client,
-      textComposer,
-      attachmentManager,
-      linkPreviewsManager,
-      pollComposer,
-      get lastChangeOriginIsLocal() {
-        return true;
-      },
-      editedMessage: undefined,
-      get quotedMessage() {
-        return undefined;
-      },
-    } as any);
+      compositionContext: channel,
+      composition: custom.editedMessage,
+    });
 
   return {
     messageComposer,
@@ -323,12 +263,9 @@ describe('stream-io/message-composer-middleware/data-validation', () => {
   });
 
   it('should not discard composition for edited message without any local change', async () => {
-    const { messageComposer, validationMiddleware } = setupMiddleware();
-    const localMessage: LocalMessage = {
+    const editedMessage: MessageResponse = {
       attachments: [],
-      created_at: new Date(),
-      deleted_at: null,
-      error: undefined,
+      created_at: new Date().toISOString(),
       id: 'test-id',
       mentioned_users: [],
       parent_id: undefined,
@@ -337,20 +274,27 @@ describe('stream-io/message-composer-middleware/data-validation', () => {
       status: 'sending',
       text: 'Hello world',
       type: 'regular',
-      updated_at: new Date(),
+      updated_at: new Date().toISOString(),
     };
-    messageComposer.editedMessage = localMessage;
+    const { messageComposer, validationMiddleware } = setupMiddleware({ editedMessage });
+
     vi.spyOn(messageComposer, 'lastChangeOriginIsLocal', 'get').mockReturnValue(false);
 
     const result = await validationMiddleware.handlers.compose(
       setupMiddlewareInputs({
         message: {
-          id: localMessage.id,
-          parent_id: localMessage.parent_id,
-          text: localMessage.text,
-          type: localMessage.type,
+          id: editedMessage.id,
+          parent_id: editedMessage.parent_id,
+          text: editedMessage.text,
+          type: editedMessage.type,
         },
-        localMessage,
+        localMessage: {
+          ...editedMessage,
+          created_at: new Date(editedMessage.created_at as string),
+          deleted_at: null,
+          pinned_at: null,
+          updated_at: new Date(editedMessage.updated_at as string),
+        } as LocalMessage,
         sendOptions: {},
       }),
     );
diff --git a/test/unit/MessageComposer/middleware/messageComposer/sharedLocation.test.ts b/test/unit/MessageComposer/middleware/messageComposer/sharedLocation.test.ts
new file mode 100644
index 000000000..81d932676
--- /dev/null
+++ b/test/unit/MessageComposer/middleware/messageComposer/sharedLocation.test.ts
@@ -0,0 +1,138 @@
+import { describe, expect, it, vi } from 'vitest';
+import {
+  createSharedLocationCompositionMiddleware,
+  DraftResponse,
+  LocalMessage,
+  MessageComposer,
+  MessageComposerMiddlewareState,
+  MiddlewareStatus,
+  StreamChat,
+} from '../../../../../src';
+
+const user = { id: 'user-id' };
+
+const setup = ({
+  composition,
+}: {
+  composition?: DraftResponse | LocalMessage;
+} = {}) => {
+  // Reset mocks
+  vi.clearAllMocks();
+
+  // Setup mocks
+  const mockClient = new StreamChat('apiKey', 'apiSecret');
+  mockClient.user = user;
+
+  const mockChannel = mockClient.channel('channelType', 'channelId');
+  mockChannel.getClient = vi.fn().mockReturnValue(mockClient);
+  const messageComposer = new MessageComposer({
+    client: mockClient,
+    composition,
+    compositionContext: mockChannel,
+    config: { location: { enabled: true } },
+  });
+  return { mockClient, mockChannel, messageComposer };
+};
+
+const setupMiddlewareHandlerParams = (
+  initialState: MessageComposerMiddlewareState = {
+    message: {},
+    localMessage: {},
+    sendOptions: {},
+  },
+) => {
+  return {
+    state: initialState,
+    next: async (state: MessageComposerMiddlewareState) => ({ state }),
+    complete: async (state: MessageComposerMiddlewareState) => ({
+      state,
+      status: 'complete' as MiddlewareStatus,
+    }),
+    discard: async () => ({ state: initialState, status: 'discard' as MiddlewareStatus }),
+    forward: async () => ({ state: initialState }),
+  };
+};
+
+describe('stream-io/message-composer-middleware/shared-location', () => {
+  it('injects shared_location to localMessage and message payloads', async () => {
+    const { messageComposer } = setup();
+    const middleware = createSharedLocationCompositionMiddleware(messageComposer);
+    const coords = { latitude: 1, longitude: 1 };
+    messageComposer.locationComposer.setData(coords);
+    const result = await middleware.handlers.compose(setupMiddlewareHandlerParams());
+    expect(result).toEqual({
+      state: {
+        localMessage: {
+          shared_location: {
+            channel_cid: messageComposer.channel.cid,
+            created_at: expect.any(String),
+            created_by_device_id: messageComposer.locationComposer.deviceId,
+            message_id: messageComposer.id,
+            updated_at: expect.any(String),
+            user_id: user.id,
+            ...coords,
+          },
+        },
+        message: {
+          shared_location: {
+            created_by_device_id: messageComposer.locationComposer.deviceId,
+            message_id: messageComposer.id,
+            ...coords,
+          },
+        },
+        sendOptions: {},
+      },
+    });
+  });
+
+  it('does not inject shared_location to localMessage and message payloads if none is set', async () => {
+    const { messageComposer } = setup();
+    const middleware = createSharedLocationCompositionMiddleware(messageComposer);
+    const result = await middleware.handlers.compose(setupMiddlewareHandlerParams());
+    expect(result).toEqual({
+      state: {
+        localMessage: {},
+        message: {},
+        sendOptions: {},
+      },
+    });
+  });
+
+  it('does not inject shared_location to localMessage and message payloads if the location state is corrupted', async () => {
+    const { messageComposer } = setup();
+    const middleware = createSharedLocationCompositionMiddleware(messageComposer);
+    // @ts-expect-error invalid location payload
+    messageComposer.locationComposer.state.next({
+      location: {
+        latitude: 1,
+        created_by_device_id: 'da',
+        message_id: messageComposer.id,
+      },
+    });
+    const result = await middleware.handlers.compose(setupMiddlewareHandlerParams());
+    expect(result).toEqual({
+      state: {
+        localMessage: {},
+        message: {},
+        sendOptions: {},
+      },
+    });
+  });
+
+  it('does not inject shared_location to localMessage and message payloads if the user is unknown', async () => {
+    const { messageComposer, mockClient } = setup();
+    const middleware = createSharedLocationCompositionMiddleware(messageComposer);
+    const coords = { latitude: 1, longitude: 1 };
+    messageComposer.locationComposer.setData(coords);
+    // @ts-expect-error setting user to invalid value
+    mockClient.user = null;
+    const result = await middleware.handlers.compose(setupMiddlewareHandlerParams());
+    expect(result).toEqual({
+      state: {
+        localMessage: {},
+        message: {},
+        sendOptions: {},
+      },
+    });
+  });
+});
diff --git a/test/unit/channel.test.js b/test/unit/channel.test.js
index c77346be8..a647ffaa7 100644
--- a/test/unit/channel.test.js
+++ b/test/unit/channel.test.js
@@ -1859,3 +1859,103 @@ describe('message sending flow', () => {
 		});
 	});
 });
+
+describe('share location', () => {
+	const userId = 'user-id';
+	const staticLocation = {
+		created_by_device_id: 'created_by_device_id',
+		latitude: 1,
+		longitude: 2,
+		message_id: 'staticLocation_message_id',
+	};
+	const liveLocation = {
+		created_by_device_id: 'created_by_device_id',
+		end_at: 'end_at',
+		latitude: 1,
+		longitude: 2,
+		message_id: 'liveLocation_message_id',
+	};
+
+	const setup = async () => {
+		const client = await getClientWithUser({ id: 'user-abc' });
+		const channel = client.channel('messaging', 'test');
+		const sendMessageSpy = vi.spyOn(channel, 'sendMessage').mockResolvedValue({});
+		const dispatchEventSpy = vi.spyOn(client, 'dispatchEvent').mockResolvedValue({});
+		const updateLocationSpy = vi.spyOn(client, 'updateLocation').mockResolvedValue({});
+		return {
+			channel,
+			client,
+			dispatchEventSpy,
+			sendMessageSpy,
+			updateLocationSpy,
+		};
+	};
+
+	it('forwards the location object', async () => {
+		const { channel, sendMessageSpy } = await setup();
+
+		await channel.sendSharedLocation(staticLocation);
+		expect(sendMessageSpy).toHaveBeenCalledWith({
+			id: staticLocation.message_id,
+			shared_location: staticLocation,
+			user: undefined,
+		});
+
+		await channel.sendSharedLocation(liveLocation);
+		expect(sendMessageSpy).toHaveBeenCalledWith({
+			id: liveLocation.message_id,
+			shared_location: liveLocation,
+			user: undefined,
+		});
+	});
+
+	it('injects the user object into the request payload', async () => {
+		const { channel, sendMessageSpy } = await setup();
+
+		await channel.sendSharedLocation(staticLocation, userId);
+		expect(sendMessageSpy).toHaveBeenCalledWith({
+			id: staticLocation.message_id,
+			shared_location: staticLocation,
+			user: { id: userId },
+		});
+
+		await channel.sendSharedLocation(liveLocation, userId);
+		expect(sendMessageSpy).toHaveBeenCalledWith({
+			id: liveLocation.message_id,
+			shared_location: liveLocation,
+			user: { id: userId },
+		});
+	});
+	it('emits live_location_sharing.started local event', async () => {
+		const { channel, dispatchEventSpy, sendMessageSpy } = await setup();
+
+		sendMessageSpy.mockResolvedValueOnce({ message: { id: staticLocation.message_id } });
+		await channel.sendSharedLocation(staticLocation);
+		expect(dispatchEventSpy).not.toHaveBeenCalled();
+
+		sendMessageSpy.mockResolvedValueOnce({ message: { id: liveLocation.message_id } });
+		await channel.sendSharedLocation(liveLocation);
+		expect(dispatchEventSpy).toHaveBeenCalledWith({
+			message: { id: liveLocation.message_id },
+			type: 'live_location_sharing.started',
+		});
+	});
+
+	it('stops live location sharing', async () => {
+		const { channel, dispatchEventSpy, updateLocationSpy } = await setup();
+
+		updateLocationSpy.mockResolvedValueOnce(staticLocation);
+		await channel.stopLiveLocationSharing(staticLocation);
+		expect(dispatchEventSpy).toHaveBeenCalledWith({
+			live_location: expect.objectContaining(staticLocation),
+			type: 'live_location_sharing.stopped',
+		});
+
+		updateLocationSpy.mockResolvedValueOnce(liveLocation);
+		await channel.stopLiveLocationSharing(liveLocation);
+		expect(dispatchEventSpy).toHaveBeenCalledWith({
+			live_location: expect.objectContaining(liveLocation),
+			type: 'live_location_sharing.stopped',
+		});
+	});
+});