# @iccandle/reactjs-widget

React overlay for the [TradingView Charting Library](https://www.tradingview.com/charting-library-docs/) that adds ICCandle’s **pattern scanner**, **results iframe**, **pattern tracker**, and **news/event** integration.

The package ships ESM + CommonJS. Component styles are injected at runtime (no separate CSS import).

## What it does

`WidgetIccandle` wraps your chart and renders:

| Surface            | Role                                                                                                                                      |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **Scanner popup**  | Draggable overlay over the chart. Select a bar window with TradingView’s Date Range tool, then run a scan or subscribe to pattern alerts. |
| **Results iframe** | Side-by-side panel hosting the ICCandle embed app (`embed-iccandle-app.iccandle.ai`) — search results, similar events, auth, billing.     |
| **Chart sync**     | Bidirectional `postMessage` bridge: pattern selection, chart replay, news marks, sign-in tokens, loading state.                           |

Typical flow:

1. User draws a **Date Range** on the chart (or activates the scanner).
2. Scanner caches the selected candles and opens the results iframe with search parameters.
3. The embed app returns matches; selecting a pattern redraws the range on the chart. Replay / news flows inject predicted candles via custom indicators.

## Install

```bash
npm install @iccandle/reactjs-widget
# or
pnpm add @iccandle/reactjs-widget
```

### Prerequisites

- **React 18+** — `react` and `react-dom` are peer dependencies.
- **TradingView Charting Library** — obtain under your own license, host the static assets (e.g. `/charting_library/`), and bootstrap the widget yourself. **This package does not ship the charting library.**
- **Auth token** — API calls (candle cache, pattern tracker) use `Bearer` auth from `localStorage` key `iccandle_token`. The embedded results app sets this via a `sign-in-success` `postMessage` after login.

## Quick start

```tsx
import { useEffect, useRef, useState } from "react";
import type {
  ChartingLibraryWidgetOptions,
  IChartingLibraryWidget,
  ResolutionString,
} from "charting_library/charting_library";
import { widget } from "charting_library/charting_library";
import {
  WidgetIccandle,
  withPlayChart,
  getCustomIndicators,
} from "@iccandle/reactjs-widget";

const LIBRARY_PATH = "/charting_library/";

function App() {
  const containerRef = useRef<HTMLDivElement>(null);
  const [chartWidget, setChartWidget] = useState<IChartingLibraryWidget | null>(
    null,
  );

  return (
    <div style={{ height: "100vh", width: "100%" }}>
      <WidgetIccandle chartWidget={chartWidget} theme="light" language="en">
        <ChartHost containerRef={containerRef} onReady={setChartWidget} />
      </WidgetIccandle>
    </div>
  );
}

function ChartHost({
  containerRef,
  chartRefs,
  onReady,
}: {
  containerRef: React.RefObject<HTMLDivElement | null>;
  chartRefs: import("@iccandle/reactjs-widget").WidgetIccandleChartRefs;
  onReady: (w: IChartingLibraryWidget | null) => void;
}) {
  useEffect(() => {
    const el = containerRef.current;
    if (!el) return;

    const options: ChartingLibraryWidgetOptions = {
      container: el,
      library_path: LIBRARY_PATH,
      symbol: "EURUSD",
      interval: "60" as ResolutionString,
      datafeed: withPlayChart(yourDatafeed), // enables chart replay injection
      locale: "en",
      autosize: true,
      drawings_access: {
        type: "black",
        tools: [{ name: "Date Range" }],
      },
      custom_indicators_getter: () => getCustomIndicators("light"),
    };

    const tv = new widget(options);
    onReady(tv);

    return () => {
      try {
        tv.remove();
      } catch {
        /* no-op */
      }
      onReady(null);
    };
  }, [containerRef, chartRefs, onReady]);

  return <div ref={containerRef} style={{ height: "100%", width: "100%" }} />;
}
```

Use a **render-prop `children`** when you need `chartRefs` for generated-candle studies (replay / predict). A plain React node also works if you do not need those refs.

For a full in-repo reference, see [`src/tradingview/TradingviewChart.tsx`](src/tradingview/TradingviewChart.tsx).

## Features

### Pattern scanner

- Subscribes to chart readiness, symbol, resolution, and drawing events.
- Manages a `date_range` multipoint drawing so the user can adjust the bar window (default window size: **25** bars).
- On scan: posts candles to `https://scan-service.iccandle.ai/cacheCandle`, then navigates the results iframe with query params (`ws`, `tk`, `et`, `symbol`, `tf`, `cid`, filters, theme).
- Optional **advanced filters** (symbols, top-k, lookback period, probability window) stored in `localStorage` under `search-filter`.

### Results iframe

- Loads `https://embed-iccandle-app.iccandle.ai/{language}?theme=...` by default.
- After scan / news navigation, the `src` updates with search or similar-events paths.
- On iframe load, the parent posts `{ type: "parent-origin", origin }` so Stripe checkout can return to the host domain. If the host URL has `?payment=success`, it also posts `{ type: "payment-success" }` so the embed can refresh subscription state.

### Pattern tracker

From the scanner popup, users can open a subscription modal to track a custom pattern (name + timeframes + symbols). Requires a valid `iccandle_token`.

### News / economic events

- Clicking a timescale mark can open an **event info** modal (currency calendar events; skips holidays / early / sentiment-only marks).
- “Go to detail” loads similar events in the iframe and draws a vertical line on the chart.
- Marks can be driven from `localStorage` keys `tv:selected-news-events` and `tv:clicked-news-event` if your datafeed implements `getTimescaleMarks` (see below).

### Chart replay (play candles)

Wrap your datafeed with `withPlayChart` and register custom indicators via `getCustomIndicators` so the embed can inject predicted / replay bars without live ticks fighting the playback.

## API

### Exports

| Name                           | Kind      | Description                                            |
| ------------------------------ | --------- | ------------------------------------------------------ |
| `WidgetIccandle`               | Component | Chart overlay + results iframe                         |
| `WidgetIccandleProps`          | Type      | Props for `WidgetIccandle`                             |
| `WidgetIccandleChartRefs`      | Type      | Refs for highlight bars / generated-candle studies     |
| `WidgetLanguage`               | Type      | Supported locale codes                                 |
| `withPlayChart`                | Function  | Wraps a TradingView datafeed for replay bar injection  |
| `getCustomIndicators`          | Function  | Returns ICCandle custom indicators (generated candles) |
| `getGeneratedCandlesMaskColor` | Function  | Theme-aware mask color for generated candles           |

### `WidgetIccandle` props

| Prop            | Type                                      | Required | Description                                                                                                                 |
| --------------- | ----------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| `chartWidget`   | `IChartingLibraryWidget \| null`          | Yes      | Live TradingView widget (`null` until ready).                                                                               |
| `children`      | `ReactNode \| ((chartRefs) => ReactNode)` | Yes      | Chart UI, or render prop receiving `WidgetIccandleChartRefs`.                                                               |
| `theme`         | `"light" \| "dark" \| "system"`           | No       | Scanner chrome + iframe theme. `"system"` follows `prefers-color-scheme`.                                                   |
| `apiKey`        | `string`                                  | No       | Publishable Plugin Portal key (`pk_live_…`). Loads the client's portal style and forwards `&apiKey=` to the results iframe. See [Theming](#theming). |
| `language`      | `WidgetLanguage`                          | No       | UI + iframe locale. One of: `en`, `zh`, `vi`, `th`, `ko`, `ja`, `mn`, `ru`. Defaults to `en`.                               |
| `onCloseResult` | `() => void`                              | No       | Called when the embed posts `close-result`.                                                                                 |
| `iframeLoaded`  | `boolean`                                 | No       | When `false`, disables scan / track actions until the results iframe is ready. Defaults to enabled behavior in the scanner. |
| `submitCallback` | `(iframeSrc: string) => void`            | Yes      | Called with the results-iframe URL when a scan runs.                                                                        |
| `isShowScanButton` | `boolean`                              | No       | Show the Scan action. Defaults to `true`. `false` is pattern mode.                                                          |
| `isShowTrackerButton` | `boolean`                           | No       | Show the Track action. Defaults to `true`.                                                                                  |
| `isShowScannerPopup` | `boolean`                            | No       | Show the scanner popup. Defaults to `true`.                                                                                 |
| `availableIntervals` | `string[]`                           | No       | Resolutions where the popup is shown and pattern ranges can be drawn. Outside the list, the popup is hidden.               |
| `onScanClick`   | `() => void`                              | No       | Called when the user clicks Scan, before config or scan logic.                                                              |

`IChartingLibraryWidget` must be imported from **your** Charting Library typings — this package does not re-export TradingView types.

### `withPlayChart(datafeed)`

Wraps `subscribeBars` so ICCandle can push replay bars and optionally block live ticks during playback.

```ts
import { withPlayChart } from "@iccandle/reactjs-widget";

datafeed: withPlayChart(myDatafeed),
```

### `getCustomIndicators(theme?)`

Returns a Promise of TradingView `CustomIndicator[]` used for generated / predicted candle overlays. Pass `"light"` or `"dark"` to match chart theme. Wire it into widget options:

```ts
custom_indicators_getter: () => getCustomIndicators("light"),
```

## Auth and storage

| Key                       | Purpose                                                                                                 |
| ------------------------- | ------------------------------------------------------------------------------------------------------- |
| `iccandle_token`          | Bearer token for scan cache, pattern tracker, and related APIs. Set by the embed via `sign-in-success`. |
| `search-filter`           | Scanner advanced options (symbols, top_k, period, probability).                                         |
| `tv:selected-news-events` | JSON array of news events used as timescale marks.                                                      |
| `tv:clicked-news-event`   | Last clicked calendar event for mark highlighting.                                                      |
| `tv:latest-symbol`        | Last chart symbol (used by the demo app / fallbacks).                                                   |

## postMessage bridge

Messages are accepted only from the results origin (`https://embed-iccandle-app.iccandle.ai`).

| Direction      | Type / action                                              | Effect                                           |
| -------------- | ---------------------------------------------------------- | ------------------------------------------------ |
| Embed → parent | `play-chart`                                               | Inject / clear replay candles on the chart       |
| Embed → parent | `loading`                                                  | Toggle scanner loading UI                        |
| Embed → parent | `sign-in-success`                                          | Persist `iccandle_token` from `payload.id_token` |
| Embed → parent | `close-result`                                             | Clears play state; calls `onCloseResult`         |
| Embed → parent | `pattern_selected` / `custom_pattern_selected`             | Draw date range for the compared pattern         |
| Embed → parent | `clear_pattern_selected` / `clear_custom_pattern_selected` | Remove pattern date range                        |
| Embed → parent | `eventClicked`                                             | Center chart, draw event line, show mark         |
| Embed → parent | `replay`                                                   | Start / stop event replay candles                |
| Embed → parent | `back` / `back-to-similar-events`                          | Clear event line / replay                        |
| Embed → parent | `nav-click`                                                | Hide scanner on `/news`; restore otherwise       |
| Parent → embed | `parent-origin`                                            | Host origin for Stripe return                    |
| Parent → embed | `payment-success`                                          | Refresh subscription after checkout              |

## Optional: timescale marks (news/events)

If your datafeed implements `getTimescaleMarks`, surface stored events as marks:

```ts
getTimescaleMarks: async (symbolInfo, from, to, onResult) => {
  const marks = [];
  try {
    const allNewsEvents = JSON.parse(
      localStorage.getItem("tv:selected-news-events") || "[]",
    );

    allNewsEvents?.forEach(({ id, timestamp, event_name, currency }) => {
      if (!id || !Number.isFinite(timestamp) || timestamp <= 0) return;
      if (marks.some((m) => String(m.id) === String(id))) return;

      marks.push({
        id,
        time: timestamp / 1000,
        color: "green",
        label: event_name.slice(0, 1) || "N",
        tooltip: [event_name],
        ...(currency ? { imageUrl: `/images/symbols/${currency}.svg` } : {}),
        showLabelWhenImageLoaded: true,
      });
    });
  } catch {
    /* ignore */
  }
  onResult(marks);
};
```

## Theming

Scanner UI uses CSS variables on `.iccandle-selector-widget`:

- `--iccandle-primary`
- `--iccandle-primary-gradient-end`
- `--iccandle-background`
- `--iccandle-border`
- `--iccandle-text`
- `--iccandle-secondary` (derived from the background)
- `--iccandle-font`

Light and dark defaults live in the bundled stylesheet. The `theme` prop toggles the `.iccandle-dark` class and is forwarded to the iframe.

### Plugin Portal style (`apiKey`)

Clients manage their style in the ICCandle Plugin Portal and create an API key there. Pass it to the widget:

```tsx
<WidgetIccandle apiKey="<KEY>" chartWidget={chartWidget} submitCallback={setIframeSrc}>
  {/* chart */}
</WidgetIccandle>
```

- The widget calls `GET https://api.iccandle.ai/plugin-client/v1/plugin/style` with the `api-key` header once per key. The result is cached in memory and refetched only when `apiKey` changes (not on theme change). The request times out after 5 s and is aborted on unmount.
- The key is also forwarded to every results-iframe URL as `&apiKey=`, so the embed app uses the same style.
- The key is **publishable**: it only reads style data and is meant to ship in client-side code. Restrict it in the portal with **allowed origins** (`scheme://host[:port]`, e.g. `https://app.example.com`). With no allowed origins any site can use the key (the portal warns about this). Requests from other origins get `403` and the widget falls back to the defaults.

Supported style fields, per theme (`light` and `dark`):

| Field                           | CSS variable                      | Accepted values                                                                                                         |
| ------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `iccandle_primary`              | `--iccandle-primary`              | `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb(r, g, b)`, `rgba(r, g, b, a)`                                             |
| `iccandle_primary_gradient_end` | `--iccandle-primary-gradient-end` | same as above                                                                                                           |
| `iccandle_background`           | `--iccandle-background`           | same as above                                                                                                           |
| `iccandle_border`               | `--iccandle-border`               | same as above                                                                                                           |
| `iccandle_text`                 | `--iccandle-text`                 | same as above                                                                                                           |
| `icc_font`                      | `--iccandle-font`                 | `Inter Tight`, `Inter`, `Roboto`, `Open Sans`, `Lato`, `Montserrat`, `Poppins`, `Nunito`, `Source Sans 3`, `IBM Plex Sans`, `system-ui` |

The portal also sets `default_theme` (`light` | `dark`). The widget re-validates every value and ignores anything outside the formats above. Non-system fonts are loaded once from Google Fonts.

**Priority** (highest first):

1. Theme: `theme` prop → portal `default_theme` → `light`.
2. Colors: portal style → bundled defaults.

**Fallback:** with no `apiKey`, an invalid or revoked key, a disallowed origin, rate limiting, no saved style, or a network error, the widget uses the bundled defaults. The chart keeps working and nothing is thrown.

## Development (this repo)

| Script        | Command               | Purpose                                         |
| ------------- | --------------------- | ----------------------------------------------- |
| Dev demo      | `pnpm run dev`        | Vite app with local charting library            |
| Library build | `pnpm run build`      | Emits `dist/` (ESM, CJS, injected CSS, `.d.ts`) |
| Web build     | `pnpm run build:web`  | SPA build to `dist-web/`                        |
| Deploy web    | `pnpm run deploy:web` | Build + Vercel production deploy                |
| Lint          | `pnpm run lint`       | ESLint                                          |
| Preview web   | `pnpm run preview`    | Preview the web build                           |

`prepublishOnly` runs `build` before npm publish.

## Troubleshooting

| Issue                          | What to check                                                                                               |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| Chart stays blank              | `library_path` must serve TradingView static files; container must be mounted before `new widget(...)`.     |
| `chartWidget` is always `null` | Call `setChartWidget` after create (or inside `onChartReady` if required).                                  |
| Scan fails / 401               | Ensure the user signed in via the results iframe so `iccandle_token` is set.                                |
| Date Range tool missing        | Enable it in `drawings_access` (see Quick start).                                                           |
| Replay bars do not appear      | Wrap the datafeed with `withPlayChart` and register `getCustomIndicators`.                                  |
| Scanner actions disabled       | Pass `iframeLoaded={true}` once the results iframe has loaded, or omit the prop if you do not gate on load. |

## License

MIT. TradingView Charting Library is subject to its own license from TradingView.
