Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

react-instantsearch-hooks

Package Overview
Dependencies
Maintainers
1
Versions
64
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-instantsearch-hooks

⚡ Lightning-fast search for React, by Algolia

  • 6.17.0
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
50K
increased by7.82%
Maintainers
1
Weekly downloads
 
Created
Source

react-instantsearch-hooks

🚧 This version is not yet production-ready.

React InstantSearch Hooks is an open-source, experimental UI library for React that lets you quickly build a search interface in your front-end application.

Installation

React InstantSearch Hooks is available on the npm registry. It relies on algoliasearch to communicate with Algolia APIs.

yarn add react-instantsearch-hooks algoliasearch
# or
npm install react-instantsearch-hooks algoliasearch

Getting started

This package exposes Hooks but no UI components (yet!). You're in charge of building components based on the exposed Hooks.

Let's start with a SearchBox component based on useSearchBox:

import React, { useEffect, useRef, useState } from 'react';
import { useSearchBox } from 'react-instantsearch-hooks';

export function SearchBox(props) {
  const { query, refine, isSearchStalled } = useSearchBox(props);
  const [value, setValue] = useState(query);
  const inputRef = useRef(null);

  function onSubmit(event) {
    event.preventDefault();
    event.stopPropagation();

    if (inputRef.current) {
      inputRef.current.blur();
    }
  }

  function onReset(event) {
    event.preventDefault();
    event.stopPropagation();

    setValue('');

    if (inputRef.current) {
      inputRef.current.focus();
    }
  }

  function onChange(event) {
    setValue(event.currentTarget.value);
  }

  useEffect(() => {
    refine(value);
  }, [refine, value]);

  useEffect(() => {
    if (query !== value) {
      setValue(query);
    }
  }, [query]);

  return (
    <div className="ais-SearchBox">
      <form
        action=""
        className="ais-SearchBox-form"
        noValidate
        onSubmit={onSubmit}
        onReset={onReset}
      >
        <input
          ref={inputRef}
          className="ais-SearchBox-input"
          autoComplete="off"
          autoCorrect="off"
          autoCapitalize="off"
          placeholder={props.placeholder}
          spellCheck={false}
          maxLength={512}
          type="search"
          value={value}
          onChange={onChange}
        />

        <button
          className="ais-SearchBox-submit"
          type="submit"
          title="Submit the search query."
        >
          <svg
            className="ais-SearchBox-submitIcon"
            width="10"
            height="10"
            viewBox="0 0 40 40"
          >
            <path d="M26.804 29.01c-2.832 2.34-6.465 3.746-10.426 3.746C7.333 32.756 0 25.424 0 16.378 0 7.333 7.333 0 16.378 0c9.046 0 16.378 7.333 16.378 16.378 0 3.96-1.406 7.594-3.746 10.426l10.534 10.534c.607.607.61 1.59-.004 2.202-.61.61-1.597.61-2.202.004L26.804 29.01zm-10.426.627c7.323 0 13.26-5.936 13.26-13.26 0-7.32-5.937-13.257-13.26-13.257C9.056 3.12 3.12 9.056 3.12 16.378c0 7.323 5.936 13.26 13.258 13.26z"></path>
          </svg>
        </button>

        <button
          className="ais-SearchBox-reset"
          type="reset"
          title="Clear the search query."
          hidden={value.length === 0 && !isSearchStalled}
        >
          <svg
            className="ais-SearchBox-resetIcon"
            viewBox="0 0 20 20"
            width="10"
            height="10"
          >
            <path d="M8.114 10L.944 2.83 0 1.885 1.886 0l.943.943L10 8.113l7.17-7.17.944-.943L20 1.886l-.943.943-7.17 7.17 7.17 7.17.943.944L18.114 20l-.943-.943-7.17-7.17-7.17 7.17-.944.943L0 18.114l.943-.943L8.113 10z"></path>
          </svg>
        </button>
      </form>
    </div>
  );
}

Then, you can create a Hits component with useHits:

import React from 'react';
import { useHits } from 'react-instantsearch-hooks';

export function Hits({ hitComponent: Hit, ...props }) {
  const { hits } = useHits(props);

  return (
    <div className="ais-Hits">
      <ol className="ais-Hits-list">
        {hits.map((hit) => (
          <li key={hit.objectID} className="ais-Hits-item">
            <Hit hit={hit} />
          </li>
        ))}
      </ol>
    </div>
  );
}

You can now use these components in the InstantSearch provider:

import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, useConnector } from 'react-instantsearch-hooks';

import { SearchBox } from './SearchBox';
import { Hits } from './Hits';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      <SearchBox />
      <Hits />
    </InstantSearch>
  );
}

Edit on CodeSandbox

You can build any InstantSearch widget using InstantSearch.js connectors with the useConnector Hook.

API

InstantSearch

The root provider component of all React InstantSearch hooks.

import { InstantSearch } from 'react-instantsearch-hooks';

It accepts all props from the InstantSearch.js instantsearch widget.

indexName

string | required

The main index to search into.

<InstantSearch
  // ...
  indexName="instant_search"
>
  {/* Widgets */}
</InstantSearch>
searchClient

object | required

Provides a search client to InstantSearch.

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

<InstantSearch
  // ...
  searchClient={searchClient}
>
  {/* Widgets */}
</InstantSearch>;
initialUiState

object

Provides an initial state to your React InstantSearch widgets using InstantSearch.js' uiState. To provide an initial state, you must add the corresponding widgets to your implementation.

<InstantSearch
  // ...
  initialUiState={{
    indexName: {
      query: 'phone',
      page: 5,
    },
  }}
>
  {/* Widgets */}
</InstantSearch>
onStateChange

function

Triggers when the state changes.

When using this option, the instance becomes controlled. This means that you become responsible for updating the UI state with setUiState.

This is useful to perform custom logic whenever the state changes.

<InstantSearch
  // ...
  onStateChange={({ uiState, setUiState }) => {
    // Custom logic

    setUiState(uiState);
  }}
>
  {/* Widgets */}
</InstantSearch>
stalledSearchDelay

number | defaults to 200

Defines a time period after which a search is considered stalled. You can find more information in the slow network guide.

<InstantSearch
  // ...
  stalledSearchDelay={500}
>
  {/* Widgets */}
</InstantSearch>
routing

boolean | object

The router configuration used to save the UI state into the URL, or any client-side persistence. You can find more information in the routing guide.

<InstantSearch
  // ...
  routing={true}
>
  {/* Widgets */}
</InstantSearch>
suppressExperimentalWarning

boolean

Removes the console warning about the experimental version. Note that this warning is only displayed in development mode.

<InstantSearch
  // ...
  suppressExperimentalWarning={true}
>
  {/* Widgets */}
</InstantSearch>

Index

The provider component for an Algolia index. It's useful when you want to build a federated search interface.

It accepts all props from the InstantSearch.js index widget.

indexName

string | required

The index to search into.

<Index indexName="instant_search">{/* Widgets */}</Index>
indexId

string

An identifier for the Index widget. Providing an indexId allows different index widgets to target the same Algolia index. It’s especially useful for the routing feature, and lets you find the refinements that match an Index widget.

<Index
  // ...
  indexId="instant_search"
>
  {/* Widgets */}
</Index>

useSearchBox

(props: UseSearchBoxProps) => SearchBoxRenderState

Hook to use a search box.

Types

UseSearchBoxProps
type UseSearchBoxProps = {
  /**
   * Function called every time the query changes.
   */
  queryHook?: (query: string, hook: (value: string) => void) => void;
};
SearchBoxRenderState
type SearchBoxRenderState = {
  /**
   * The query from the last search.
   */
  query: string;
  /**
   * Sets a new query and searches.
   */
  refine: (value: string) => void;
  /**
   * Clears the query and searches.
   */
  clear: () => void;
  /**
   * Whether the search results take more than a certain time to come back from
   * Algolia servers.
   * This can be configured on <InstantSearch /> with the `stalledSearchDelay`
   * props which defaults to 200ms.
   */
  isSearchStalled: boolean;
};

Example

function SearchBox(props) {
  const { query, refine } = useSearchBox(props);

  return {
    /* Markup */
  };
}

useHits

(props: UseHitsProps) => HitsRenderState

Hook to use hits.

Types

UseHitsProps
type UseHitsProps = {
  /**
   * Whether to escape HTML tags from hits string values.
   * @default true
   */
  escapeHTML?: boolean;
  /**
   * Function to transform the items passed to the templates.
   */
  transformItems?: (items: TItem[]) => TItem[];
};
HitsRenderState
type HitsRenderState = {
  /**
   * The matched hits from Algolia API.
   */
  hits: Hits;
  /**
   * The response from the Algolia API.
   */
  results?: SearchResults;
  /**
   * Sends an event to the Insights middleware.
   */
  sendEvent: (eventType: string, hits: Hit | Hits, eventName?: string) => void;
};

Example

function Hits(props) {
  const { hits } = useHits(props);

  return {
    /* Markup */
  };
}

useMenu

(props: UseMenuProps) => MenuRenderState

Hook to use a menu.

Types

UseMenuProps
type UseMenuProps = {
  /**
   * The name of the attribute in the records.
   */
  attribute: string;
  /**
   * The max number of items to display when
   * `showMoreLimit` is not set or if the widget is showing less value.
   */
  limit?: number;
  /**
   * Whether to display a button that expands the number of items.
   */
  showMore?: boolean;
  /**
   * The max number of items to display if the widget
   * is showing more items.
   */
  showMoreLimit?: number;
  /**
   * How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
   *
   * You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
   *
   * If a facetOrdering is set in the index settings, it is used when sortBy isn't passed
   */
  sortBy?: SortBy<MenuItem>;
  /**
   * Function to transform the items passed to the templates.
   */
  transformItems?: TransformItems<MenuItem>;
};
MenuRenderState
export type MenuItem = {
  /**
   * The value of the refinement list item.
   */
  value: string;
  /**
   * Human-readable value of the refinement list item.
   */
  label: string;
  /**
   * Number of matched results after refinement is applied.
   */
  count: number;
  /**
   * Indicates if the list item is refined.
   */
  isRefined: boolean;
};

type MenuRenderState = {
  /**
   * The list of filtering values returned from Algolia API.
   */
  items: MenuItem[];
  /**
   * Creates the next state url for a selected refinement.
   */
  createURL: (value: string) => string;
  /**
   * Action to apply selected refinements.
   */
  refine(value: string): void;
  /**
   * Send event to insights middleware
   */
  sendEvent: (
    eventType: string,
    facetValue: string,
    eventName?: string
  ) => void;
  /**
   * `true` if a refinement can be applied.
   */
  canRefine: boolean;
  /**
   * `true` if the toggleShowMore button can be activated (enough items to display more or
   * already displaying more than `limit` items)
   */
  canToggleShowMore: boolean;
  /**
   * True if the menu is displaying all the menu items.
   */
  isShowingMore: boolean;
  /**
   * Toggles the number of values displayed between `limit` and `showMoreLimit`.
   */
  toggleShowMore: () => void;
};

Example

function Menu(props) {
  const { items } = useMenu(props);

  return {
    /* Markup */
  };
}

useHierarchicalMenu

(props: UseHierarchicalMenuProps) => HierarchicalMenuRenderState

Hook to use a hierarchical menu.

Types

UseHierarchicalMenuProps
type UseHierarchicalMenuProps = {
  /**
   * The name of the attributes in the records.
   */
  attributes: string[];
  /**
   * Separator used in the attributes to separate level values.
   */
  separator?: string;
  /**
   * Prefix path to use if the first level is not the root level.
   */
  rootPath?: string | null;
  /**
   * Show the siblings of the selected parent levels of the current refined value. This
   * does not impact the root level.
   */
  showParentLevel?: boolean;
  /**
   * The max number of items to display when
   * `showMoreLimit` is not set or if the widget is showing less value.
   */
  limit?: number;
  /**
   * Whether to display a button that expands the number of items.
   */
  showMore?: boolean;
  /**
   * The max number of items to display if the widget
   * is showing more items.
   */
  showMoreLimit?: number;
  /**
   * How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
   *
   * You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
   *
   * If a facetOrdering is set in the index settings, it is used when sortBy isn't passed
   */
  sortBy?: SortBy<HierarchicalMenuItem>;
  /**
   * Function to transform the items passed to the templates.
   */
  transformItems?: TransformItems<HierarchicalMenuItem>;
};
HierarchicalMenuRenderState
export type HierarchicalMenuItem = {
  /**
   * The value of the refinement list item.
   */
  value: string;
  /**
   * Human-readable value of the refinement list item.
   */
  label: string;
  /**
   * Human-readable value of the searched refinement list item.
   */
  highlighted?: string;
  /**
   * Number of matched results after refinement is applied.
   */
  count: number;
  /**
   * Indicates if the list item is refined.
   */
  isRefined: boolean;
  /**
   * n+1 level of items, same structure HierarchicalMenuItem
   */
  data: HierarchicalMenuItem[] | null;
};

type HierarchicalMenuRenderState = {
  /**
   * The list of filtering values returned from Algolia API.
   */
  items: HierarchicalMenuItem[];
  /**
   * Creates the next state url for a selected refinement.
   */
  createURL: (value: string) => string;
  /**
   * Action to apply selected refinements.
   */
  refine(value: string): void;
  /**
   * Send event to insights middleware
   */
  sendEvent: (
    eventType: string,
    facetValue: string,
    eventName?: string
  ) => void;
  /**
   * `true` if a refinement can be applied.
   */
  canRefine: boolean;
  /**
   * `true` if the toggleShowMore button can be activated (enough items to display more or
   * already displaying more than `limit` items)
   */
  canToggleShowMore: boolean;
  /**
   * True if the menu is displaying all the menu items.
   */
  isShowingMore: boolean;
  /**
   * Toggles the number of values displayed between `limit` and `showMoreLimit`.
   */
  toggleShowMore: () => void;
};

Example

function HierarchicalMenu(props) {
  const { items } = useHierarchicalMenu(props);

  return {
    /* Markup */
  };
}

useRange

(props: UseRangeProps) => RangeRenderState

Hook to use a range.

Types

UseRangeProps
type UseRangeProps = {
  /**
   * The name of the attribute in the records.
   */
  attribute: string;
  /**
   * Minimal range value, default to automatically computed from the result set.
   */
  min?: number;
  /**
   * Maximal range value, default to automatically computed from the result set.
   */
  max?: number;
  /**
   * Number of digits after decimal point to use.
   */
  precision?: number;
};
RangeRenderState
type RangeRenderState = {
  /**
   * Sets a range to filter the results on. Both values
   * are optional, and will default to the higher and lower bounds. You can use `undefined` to remove a
   * previously set bound or to set an infinite bound.
   * @param rangeValue tuple of [min, max] bounds
   */
  refine(rangeValue: RangeBoundaries): void;
  /**
   * Indicates whether this widget can be refined
   */
  canRefine: boolean;
  /**
   * Send event to insights middleware
   */
  sendEvent(eventType: string, facetValue: string, eventName?: string): void;
  /**
   * Maximum range possible for this search
   */
  range: Range;
  /**
   * Current refinement of the search
   */
  start: RangeBoundaries;
  /**
   * Transform for the rendering `from` and/or `to` values.
   * Both functions take a `number` as input and should output a `string`.
   */
  format: {
    from(fromValue: number): string;
    to(toValue: number): string;
  };
};

Example

function Range(props) {
  const {
    range: { min, max },
    start: [minValue, maxValue],
  } = useRange(props);

  return {
    /* Markup */
  };
}

useRefinementList

(props: UseRefinementListProps) => RefinementListRenderState

Hook to use a refinement list.

Types

UseRefinementListProps
type UseRefinementListProps = {
  /**
   * The name of the attribute in the records.
   */
  attribute: string;
  /**
   * How the filters are combined together.
   */
  operator?: 'and' | 'or';
  /**
   * The max number of items to display when
   * `showMoreLimit` is not set or if the widget is showing less value.
   */
  limit?: number;
  /**
   * Whether to display a button that expands the number of items.
   */
  showMore?: boolean;
  /**
   * The max number of items to display if the widget
   * is showing more items.
   */
  showMoreLimit?: number;
  /**
   * How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
   *
   * You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax).
   *
   * If a facetOrdering is set in the index settings, it is used when sortBy isn't passed
   */
  sortBy?: SortBy<RefinementListItem>;
  /**
   * Escapes the content of the facet values.
   */
  escapeFacetValues?: boolean;
  /**
   * Function to transform the items passed to the templates.
   */
  transformItems?: TransformItems<RefinementListItem>;
};
RefinementListRenderState
export type RefinementListItem = {
  /**
   * The value of the refinement list item.
   */
  value: string;
  /**
   * Human-readable value of the refinement list item.
   */
  label: string;
  /**
   * Human-readable value of the searched refinement list item.
   */
  highlighted?: string;
  /**
   * Number of matched results after refinement is applied.
   */
  count: number;
  /**
   * Indicates if the list item is refined.
   */
  isRefined: boolean;
};

type RefinementListRenderState = {
  /**
   * The list of filtering values returned from Algolia API.
   */
  items: RefinementListItem[];
  /**
   * indicates whether the results are exhaustive (complete)
   */
  hasExhaustiveItems: boolean;
  /**
   * Creates the next state url for a selected refinement.
   */
  createURL: (value: string) => string;
  /**
   * Action to apply selected refinements.
   */
  refine(value: string): void;
  /**
   * Send event to insights middleware
   */
  sendEvent: (
    eventType: string,
    facetValue: string,
    eventName?: string
  ) => void;
  /**
   * Searches for values inside the list.
   */
  searchForItems: (query: string) => void;
  /**
   * `true` if the values are from an index search.
   */
  isFromSearch: boolean;
  /**
   * `true` if a refinement can be applied.
   */
  canRefine: boolean;
  /**
   * `true` if the toggleShowMore button can be activated (enough items to display more or
   * already displaying more than `limit` items)
   */
  canToggleShowMore: boolean;
  /**
   * True if the menu is displaying all the menu items.
   */
  isShowingMore: boolean;
  /**
   * Toggles the number of values displayed between `limit` and `showMoreLimit`.
   */
  toggleShowMore: () => void;
};

Example

function RefinementList(props) {
  const { items } = useRefinementList(props);

  return {
    /* Markup */
  };
}

useSortBy

(props: UseSortByProps) => SortByRenderState

Hook to sort by specified indices.

Types

SortByItem
type SortByItem = {
  /**
   * The name of the index to target.
   */
  value: string;
  /**
   * The label of the index to display.
   */
  label: string;
};
UseSortByProps
type UseSortByProps = {
  /**
   * Array of objects defining the different indices to choose from.
   */
  items: SortByItem[];
  /**
   * Function to transform the items passed to the templates.
   */
  transformItems?: TransformItems<SortByItem>;
};
SortByRenderState
type SortByRenderState = {
  /**
   * The initially selected index.
   */
  initialIndex?: string;
  /**
   * The currently selected index.
   */
  currentRefinement: string;
  /**
   * All the available indices
   */
  options: SortByItem[];
  /**
   * Switches indices and triggers a new search.
   */
  refine: (value: string) => void;
  /**
   * `true` if the last search contains no result.
   */
  hasNoResults: boolean;
};

Example

function SortBy(props) {
  const { currentRefinement, options, refine } = useSortBy(props);

  return {
    /* Markup */
  };
}

useQueryRules

(props?: UseQueryRulesProps) => QueryRulesRenderState

Hook to query rules.

Types

UseQueryRulesProps
type UseQueryRulesProps = {
  trackedFilters?: ParamTrackedFilters;
  transformRuleContexts?: ParamTransformRuleContexts;
  transformItems?: ParamTransformItems;
}
QueryRulesRenderState
type QueryRulesRenderState = {
  items: any[];
};
ParamTrackedFilters
type ParamTrackedFilters = {
  [facetName: string]: (
    facetValues: TrackedFilterRefinement[]
  ) => TrackedFilterRefinement[];
};
TrackedFilterRefinement
type TrackedFilterRefinement = string | number | boolean;
ParamTransformRuleContexts
type ParamTransformRuleContexts = (ruleContexts: string[]) => string[];
ParamTransformItems
type ParamTransformItems = TransformItems<any>;
function QueryRuleContext(props) {
  const { items } = useQueryRules({
    trackedFilters: {
      genre: () => ['Comedy', 'Thriller'],
    },
  });
  return null;
}

function QueryRuleCustomData(props) {
  const { items } = useQueryRules(props);

  return (
    <>
      {items.map((item) => (
        <a href={item.link}>{item.title}</a>
      ))}
    </>
  );
}

useHitsPerPage

(props: UseHitsPerPageProps) => HitsPerPageRenderState

Hook to hits per page.

Types

UseHitsPerPageProps
type UseHitsPerPageProps = {
  /**
   * Array of objects defining the different indices to choose from.
   */
  items: HitsPerPageConnectorParamsItem[];
  /**
   * Function to transform the items passed to the templates.
   */
  transformItems?: (items: HitsPerPageRenderStateItem[]) => HitsPerPageRenderStateItem[];
}
HitsPerPageConnectorParamsItem
type HitsPerPageConnectorParamsItem = {
  /**
   * Label to display in the option.
   */
  label: string;
  /**
   * Number of hits to display per page.
   */
  value: number;
  /**
   * The default hits per page on first search.
   *
   * @default false
   */
  default?: boolean;
};
HitsPerPageRenderState
type HitsPerPageRenderState = {
  /**
   * Array of objects defining the different values and labels.
   */
  items: HitsPerPageRenderStateItem[];
  /**
   * Sets the number of hits per page and triggers a search.
   */
  refine: (value: number) => void;
  /**
   * Indicates whether or not the search has results.
   */
  hasNoResults: boolean;
};
HitsPerPageRenderStateItem
type HitsPerPageRenderStateItem = {
  /**
   * Label to display in the option.
   */
  label: string;
  /**
   * Number of hits to display per page.
   */
  value: number;
  /**
   * Indicates if it's the current refined value.
   */
  isRefined: boolean;
};

Example

function HitsPerPage(props) {
  const { items, refine, hasNoResults } = useHitsPerPage(props);

  return {
    /* Markup */
  };
}

useConnector

<TProps, TWidgetDescription>(connector: Connector<TWidgetDescription, TProps>, props: TProps) => TWidgetDescription['renderState']

React Hook to plug an InstantSearch.js connector to React InstantSearch.

Here's an example to use connectMenu:

import connectMenu from 'instantsearch.js/es/connectors/menu/connectMenu';
import { useConnector } from 'react-instantsearch-hooks';

function useMenu(props) {
  return useConnector(connectMenu, props);
}

If you use TypeScript:

import connectMenu, {
  MenuConnectorParams,
  MenuWidgetDescription,
} from 'instantsearch.js/es/connectors/menu/connectMenu';
import { useConnector } from './useConnector';

type UseMenuProps = MenuConnectorParams;

function useMenu(props: UseMenuProps) {
  return useConnector<MenuConnectorParams, MenuWidgetDescription>(
    connectMenu,
    props
  );
}

Keywords

FAQs

Package last updated on 08 Dec 2021

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc