@likyly/react 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -130
- package/package.json +22 -2
package/README.md
CHANGED
|
@@ -1,44 +1,27 @@
|
|
|
1
1
|
# @likyly/react
|
|
2
2
|
|
|
3
|
-
React
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
server-side-only integrations that never render anything.
|
|
13
|
-
- This repo's own convention is one package per surface (`sdk/js`, `sdk/python`, `sdk/php`,
|
|
14
|
-
`sdk/mcp`, ...) - `@likyly/react` is that same pattern applied to a second JS surface, not a
|
|
15
|
-
new idea.
|
|
16
|
-
- It matches how this kind of binding is normally split in the ecosystem (a core client plus a
|
|
17
|
-
thin framework package: `stripe` / `@stripe/react-stripe-js`, `algoliasearch` /
|
|
18
|
-
`react-instantsearch`).
|
|
19
|
-
- `@likyly/react` depends on `@likyly/sdk` and adds **zero** wire/HTTP logic of its own - every
|
|
20
|
-
method here calls straight through to the `Likyly` client underneath (see `src/controller.ts`,
|
|
21
|
-
the plain-JS state machine both `useRecommendations` and `<LikylyRecommendations>` share - it
|
|
22
|
-
has no React import at all, which is also why it's unit-tested with no DOM/renderer needed).
|
|
3
|
+
Show LIKYLY recommendations in a React app, with the impressions and clicks LIKYLY learns from
|
|
4
|
+
tracked for you.
|
|
5
|
+
|
|
6
|
+
The fastest way to set it up is the wizard, which installs this package, writes your public key
|
|
7
|
+
and connects your AI coding assistant:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx @likyly/wizard
|
|
11
|
+
```
|
|
23
12
|
|
|
24
13
|
## Install
|
|
25
14
|
|
|
26
15
|
```sh
|
|
27
|
-
npm install @likyly/react
|
|
16
|
+
npm install @likyly/react
|
|
28
17
|
```
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
> to npm), this package's `dependencies` point at it via a relative `file:../js` reference.
|
|
32
|
-
> Once published, that becomes a normal semver range - nothing a consumer needs to think about.
|
|
33
|
-
> One consequence in the meantime: npm installs a `file:` dependency as a symlink pointing
|
|
34
|
-
> outside the consuming app's own folder, which **Turbopack fails to resolve** ("Module not
|
|
35
|
-
> found"). Run `next dev`/`next build` with `--webpack` until both packages are on npm - see
|
|
36
|
-
> `examples/demo-storefront` for a working setup with this already applied.
|
|
19
|
+
React 18 or 19. Works with Next.js (App Router included), Vite, Remix and others.
|
|
37
20
|
|
|
38
21
|
## Quick start
|
|
39
22
|
|
|
40
23
|
```tsx
|
|
41
|
-
import { LikylyProvider, LikylyRecommendations } from "@likyly/react";
|
|
24
|
+
import { LikylyProvider, LikylyRecommendations, useLikyly } from "@likyly/react";
|
|
42
25
|
|
|
43
26
|
export function App() {
|
|
44
27
|
return (
|
|
@@ -48,133 +31,76 @@ export function App() {
|
|
|
48
31
|
);
|
|
49
32
|
}
|
|
50
33
|
|
|
51
|
-
function ProductPage({ product, user
|
|
34
|
+
function ProductPage({ product, user }) {
|
|
35
|
+
const { session } = useLikyly();
|
|
52
36
|
return (
|
|
53
|
-
<LikylyRecommendations
|
|
54
|
-
|
|
55
|
-
itemId={product.id}
|
|
56
|
-
userId={user?.id}
|
|
57
|
-
sessionId={sessionId}
|
|
58
|
-
limit={4}
|
|
59
|
-
/>
|
|
37
|
+
<LikylyRecommendations placement="pdp-related" itemId={product.id} limit={4}
|
|
38
|
+
userId={user?.id} sessionId={session.getAnonymousId()} />
|
|
60
39
|
);
|
|
61
40
|
}
|
|
62
41
|
```
|
|
63
42
|
|
|
64
|
-
`apiKey`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
43
|
+
- `apiKey` is your **public key** (`pk_…`). A secret or developer key is refused in the browser.
|
|
44
|
+
- `pdp-related` is a placement you create once in LIKYLY (console or AI assistant), not in code.
|
|
45
|
+
- `sessionId` is the visitor's anonymous id, kept in the browser (first-party, no cookie). Pass it
|
|
46
|
+
so visitors who are not signed in count too.
|
|
68
47
|
|
|
69
|
-
|
|
70
|
-
(`create_placement`) or dashboard - never from this package, which is intentionally
|
|
71
|
-
runtime-only. `get_placement_requirements` tells you exactly which of `itemId`/`userId`/
|
|
72
|
-
`sessionId`/... a given placement actually needs.
|
|
48
|
+
## Your own markup
|
|
73
49
|
|
|
74
|
-
|
|
50
|
+
Keep your own cards; impressions and clicks are still tracked:
|
|
75
51
|
|
|
76
|
-
|
|
52
|
+
```tsx
|
|
53
|
+
<LikylyRecommendations
|
|
54
|
+
placement="pdp-related"
|
|
55
|
+
itemId={product.id}
|
|
56
|
+
renderItem={(item, { onClick }) => <ProductCard product={item} onClick={onClick} />}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or take full control with the hook, and report what was seen and clicked:
|
|
77
61
|
|
|
78
62
|
```tsx
|
|
79
|
-
import { useRecommendations } from "@likyly/react";
|
|
63
|
+
import { useLikyly, useRecommendations } from "@likyly/react";
|
|
80
64
|
|
|
81
|
-
function
|
|
82
|
-
const {
|
|
65
|
+
function Related({ product }) {
|
|
66
|
+
const { session } = useLikyly();
|
|
67
|
+
const { items, isLoading, trackImpression, trackClick } = useRecommendations({
|
|
83
68
|
placement: "pdp-related",
|
|
84
|
-
context: { itemId: product.id,
|
|
69
|
+
context: { itemId: product.id, sessionId: session.getAnonymousId() },
|
|
85
70
|
limit: 4,
|
|
86
71
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<MyOwnCard
|
|
93
|
-
key={item.itemId}
|
|
94
|
-
item={item}
|
|
95
|
-
onVisible={() => trackImpression(item.itemId)}
|
|
96
|
-
onClick={() => trackClick(item.itemId)}
|
|
97
|
-
/>
|
|
98
|
-
))}
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
72
|
+
if (isLoading) return null;
|
|
73
|
+
return items.map((item) => (
|
|
74
|
+
<ProductCard key={item.itemId} product={item}
|
|
75
|
+
onVisible={() => trackImpression(item.itemId)} onClick={() => trackClick(item.itemId)} />
|
|
76
|
+
));
|
|
101
77
|
}
|
|
102
78
|
```
|
|
103
79
|
|
|
104
|
-
|
|
105
|
-
observe, so it can't tell what the visitor actually saw. Call `trackImpression` once your own
|
|
106
|
-
card is actually visible (e.g. from your own `IntersectionObserver`, or eagerly on render if
|
|
107
|
-
that's an acceptable approximation for your UI), and `trackClick` from your card's click handler.
|
|
108
|
-
|
|
109
|
-
## Level 2: ready-to-use (`<LikylyRecommendations>`)
|
|
80
|
+
## Other events
|
|
110
81
|
|
|
111
|
-
|
|
112
|
-
fired when a card is actually scrolled into view, not merely fetched) and `recommendationClick`.
|
|
113
|
-
Doesn't lock you out of styling it:
|
|
114
|
-
|
|
115
|
-
```tsx
|
|
116
|
-
// Default markup, styled via className / the data-likyly-* attributes it sets:
|
|
117
|
-
<LikylyRecommendations placement="pdp-related" itemId={product.id} userId={user?.id} className="related-grid" />
|
|
118
|
-
|
|
119
|
-
// Full control over each card, while impression/click tracking stays automatic:
|
|
120
|
-
<LikylyRecommendations
|
|
121
|
-
placement="pdp-related"
|
|
122
|
-
itemId={product.id}
|
|
123
|
-
userId={user?.id}
|
|
124
|
-
renderItem={(item, { onClick }) => (
|
|
125
|
-
<MyExistingProductCard product={item} onClick={onClick} />
|
|
126
|
-
)}
|
|
127
|
-
/>
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## What's never auto-tracked
|
|
131
|
-
|
|
132
|
-
`product_view`, `add_to_cart` and `purchase` are **never** fired by this package, on purpose -
|
|
133
|
-
LIKYLY has no reliable, universal way to know when your app navigated to a product page, added
|
|
134
|
-
something to a cart, or completed a checkout across every possible stack. Instrument these
|
|
135
|
-
yourself with the plain SDK helpers, at the point in your own code where each actually happens:
|
|
82
|
+
Product views, add-to-cart and purchases happen in your own code, so send them from there:
|
|
136
83
|
|
|
137
84
|
```ts
|
|
138
|
-
|
|
139
|
-
await
|
|
140
|
-
await
|
|
85
|
+
const { client } = useLikyly();
|
|
86
|
+
await client.events.productView({ itemId: product.id, userId: user?.id });
|
|
87
|
+
await client.events.addToCart({ itemId: product.id, userId: user?.id, quantity: 1 });
|
|
88
|
+
await client.events.purchase({ eventId: `purchase_${order.id}_${item.id}`, itemId: item.id, userId: user?.id,
|
|
89
|
+
quantity: item.quantity, properties: { price: item.price, currency: "EUR" } });
|
|
141
90
|
```
|
|
142
91
|
|
|
143
|
-
|
|
144
|
-
for exactly which events a given placement needs, and `get_placement_health` /
|
|
145
|
-
`validate_integration` to confirm they're actually arriving once wired up.
|
|
92
|
+
Set `eventId` on purchases: a retried purchase is then never counted twice.
|
|
146
93
|
|
|
147
|
-
##
|
|
94
|
+
## Sign-in
|
|
148
95
|
|
|
149
|
-
|
|
150
|
-
is a first-party, cookieless anonymous id - no need to invent your own:
|
|
96
|
+
At sign-in, attach what the visitor did anonymously to their account:
|
|
151
97
|
|
|
152
98
|
```tsx
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
function LoginForm() {
|
|
156
|
-
const { client, session } = useLikyly();
|
|
157
|
-
async function onLogin(user) {
|
|
158
|
-
await session.identify(client, user.id); // attaches this session's past interactions to them
|
|
159
|
-
}
|
|
160
|
-
}
|
|
99
|
+
const { client, session } = useLikyly();
|
|
100
|
+
await session.identify(client, user.id);
|
|
161
101
|
```
|
|
162
102
|
|
|
163
|
-
##
|
|
164
|
-
|
|
165
|
-
- `<LikylyProvider apiKey>` must be the **public** key. `@likyly/sdk`'s `Likyly` constructor
|
|
166
|
-
throws immediately if it detects a secret/developer-shaped key (`sk_`/`lk_` prefix) running
|
|
167
|
-
where `window`/`document` exist - i.e. exactly the "this ended up in a browser" mistake.
|
|
168
|
-
- Never read that key from a server-only env var, and never name the env var something like
|
|
169
|
-
`NEXT_PUBLIC_LIKYLY_SECRET_KEY` - only the public key belongs in a `NEXT_PUBLIC_*`/
|
|
170
|
-
`VITE_PUBLIC_*`-style variable at all.
|
|
171
|
-
- This package has no method that accepts or needs a secret/developer key - there is
|
|
172
|
-
structurally nothing here that would tempt using one. Placement/data-source configuration is
|
|
173
|
-
the LIKYLY MCP admin tools' job (secret/developer key, server-side, from your coding agent),
|
|
174
|
-
never this runtime package's.
|
|
175
|
-
|
|
176
|
-
## Requirements
|
|
103
|
+
## Links
|
|
177
104
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
`useRecommendations` from a Client Component.
|
|
105
|
+
- [`@likyly/sdk`](https://www.npmjs.com/package/@likyly/sdk): the JavaScript client this package is built on
|
|
106
|
+
- [likyly.com](https://likyly.com)
|
package/package.json
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@likyly/react",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "LIKYLY product recommendations for React and Next.js: a ready-made component and a hook, with impressions and clicks tracked for you.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"likyly",
|
|
7
|
+
"react",
|
|
8
|
+
"nextjs",
|
|
9
|
+
"recommendations",
|
|
10
|
+
"product recommendations",
|
|
11
|
+
"personalization",
|
|
12
|
+
"ecommerce",
|
|
13
|
+
"hooks"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://www.likyly.com",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Likyly/likyly-recsys.git",
|
|
19
|
+
"directory": "sdk/react"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Likyly/likyly-recsys/issues"
|
|
23
|
+
},
|
|
24
|
+
"author": "LIKYLY (https://www.likyly.com)",
|
|
5
25
|
"license": "MIT",
|
|
6
26
|
"type": "module",
|
|
7
27
|
"sideEffects": false,
|