@betterreviews/react-native 1.0.0
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/LICENSE +145 -0
- package/README.md +189 -0
- package/SECURITY.md +238 -0
- package/dist/index.d.mts +581 -0
- package/dist/index.d.ts +581 -0
- package/dist/index.js +2384 -0
- package/dist/index.mjs +2346 -0
- package/package.json +78 -0
- package/src/BetterReviewsProvider.tsx +62 -0
- package/src/ProductContentBlock.tsx +143 -0
- package/src/StarRating.tsx +85 -0
- package/src/WebViewHost.tsx +164 -0
- package/src/bridge.ts +48 -0
- package/src/client/createBetterReviewsClient.ts +211 -0
- package/src/client/types.ts +101 -0
- package/src/icons/BRIcons.tsx +176 -0
- package/src/index.ts +74 -0
- package/src/minSdkVersion.ts +52 -0
- package/src/sections/FeaturesSection.tsx +69 -0
- package/src/sections/ReviewsSummarySection.tsx +47 -0
- package/src/telemetry.ts +52 -0
- package/src/theme/applyTheme.ts +72 -0
- package/src/theme/widgetTheme.ts +67 -0
- package/src/webviewMessage.ts +23 -0
- package/src/widget/ReviewWidget.tsx +230 -0
- package/src/widget/WidgetContext.tsx +43 -0
- package/src/widget/components/FilterToolbar.tsx +146 -0
- package/src/widget/components/MediaGallery.tsx +53 -0
- package/src/widget/components/PulseSection.tsx +69 -0
- package/src/widget/components/RatingStars.tsx +40 -0
- package/src/widget/components/ReviewCard.tsx +114 -0
- package/src/widget/components/SortDrawer.tsx +49 -0
- package/src/widget/components/StaleListOverlay.tsx +51 -0
- package/src/widget/components/VoteButtons.tsx +55 -0
- package/src/widget/hooks/useReviewDetail.ts +55 -0
- package/src/widget/hooks/useReviewList.ts +136 -0
- package/src/widget/hooks/useReviewSummary.ts +24 -0
- package/src/widget/hooks/useVote.ts +68 -0
- package/src/widget/styles.ts +393 -0
- package/src/widget/util.ts +21 -0
- package/src/widget/viewer/MediaReviewViewer.tsx +350 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import * as v from 'valibot';
|
|
3
|
+
import { WebView } from 'react-native-webview';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `betterreviews_reactiv.config` (v1)
|
|
7
|
+
*
|
|
8
|
+
* Per-product render config. Signals "merchant has configured this
|
|
9
|
+
* product" so the RN package can fall back gracefully when no
|
|
10
|
+
* ProductContentBlock data exists.
|
|
11
|
+
*
|
|
12
|
+
* Schema SHAPE-OF-TRUTH: matches `PPO.Reactiv.ContentResolver.resolve_for_store/2`
|
|
13
|
+
* config emit shape today. The proposal doc mentions `bridge` and
|
|
14
|
+
* `min_sdk_version` as future fields; the resolver does not emit them
|
|
15
|
+
* yet. Both are forward-compat OPTIONAL so the schema accepts future
|
|
16
|
+
* resolver versions without a schema bump.
|
|
17
|
+
*
|
|
18
|
+
* See `docs/proposals/reactiv-schema-2026-05-20.md` § 10 for the
|
|
19
|
+
* future-state shape (bridge / min_sdk_version land in Card C.14 +
|
|
20
|
+
* post-soft-launch RN work).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
declare const configSchema: v.ObjectSchema<{
|
|
24
|
+
readonly v: v.LiteralSchema<1, undefined>;
|
|
25
|
+
readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, undefined>]>;
|
|
26
|
+
readonly product_content_block_enabled: v.BooleanSchema<undefined>;
|
|
27
|
+
readonly min_sdk_version: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "min_sdk_version must be valid semver">]>, undefined>;
|
|
28
|
+
readonly bridge: v.OptionalSchema<v.PicklistSchema<["auto", "required", "off"], undefined>, undefined>;
|
|
29
|
+
}, undefined>;
|
|
30
|
+
type Config = v.InferOutput<typeof configSchema>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `betterreviews_reactiv.theme` (v1)
|
|
34
|
+
*
|
|
35
|
+
* Theme inherited from the merchant's `review_settings.conversation_theme`.
|
|
36
|
+
* Colors are passed through `PPO.Reactiv.Color.normalize/1` on the
|
|
37
|
+
* Elixir side (rgb/rgba/hsl/hex input → canonical hex output) before
|
|
38
|
+
* being emitted by the resolver.
|
|
39
|
+
*
|
|
40
|
+
* Schema SHAPE-OF-TRUTH: matches `PPO.Reactiv.ContentResolver.build_theme/1`
|
|
41
|
+
* + `PPOWeb.Live.ThemeHelpers.@font_map`/@corner_map. The
|
|
42
|
+
* `docs/proposals/reactiv-schema-2026-05-20.md` § 10 sketch proposed a
|
|
43
|
+
* richer nested `palette`/`typography`/`corners` structure; the
|
|
44
|
+
* implemented resolver emits the flatter shape below. Proposal-vs-impl
|
|
45
|
+
* drift documented in `schemas/betterreviews-reactiv/DESIGN.md`.
|
|
46
|
+
*
|
|
47
|
+
* All theme fields except `v` are OPTIONAL — the resolver emits a
|
|
48
|
+
* field only when the corresponding store-level theme value is set
|
|
49
|
+
* (`maybe_put_color`/`maybe_put` semantics).
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
declare const themeSchema: v.ObjectSchema<{
|
|
53
|
+
readonly v: v.LiteralSchema<1, undefined>;
|
|
54
|
+
readonly primary_color: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "Color must be hex #RRGGBB (case-insensitive)">]>, undefined>;
|
|
55
|
+
readonly background_color: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "Color must be hex #RRGGBB (case-insensitive)">]>, undefined>;
|
|
56
|
+
readonly text_color: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "Color must be hex #RRGGBB (case-insensitive)">]>, undefined>;
|
|
57
|
+
readonly accent_color: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "Color must be hex #RRGGBB (case-insensitive)">]>, undefined>;
|
|
58
|
+
readonly corner_style: v.OptionalSchema<v.PicklistSchema<["sharp", "slightly-rounded", "rounded", "extra-rounded"], undefined>, undefined>;
|
|
59
|
+
readonly font_family: v.OptionalSchema<v.PicklistSchema<["system", "serif", "sans-serif", "mono"], undefined>, undefined>;
|
|
60
|
+
}, undefined>;
|
|
61
|
+
type Theme = v.InferOutput<typeof themeSchema>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Telemetry event types emitted by the package. Per SECURITY.md, all
|
|
65
|
+
* events are pre-redacted — no customer email, name, IP, transcript,
|
|
66
|
+
* or review content appears in any payload.
|
|
67
|
+
*
|
|
68
|
+
* Consumers (partner host apps) wire `onTelemetryEvent` on the
|
|
69
|
+
* `<BetterReviewsProvider>` to forward events to their own
|
|
70
|
+
* observability stack.
|
|
71
|
+
*/
|
|
72
|
+
type TelemetryEvent = {
|
|
73
|
+
type: 'betterreviews.fetch.success';
|
|
74
|
+
block_id?: string;
|
|
75
|
+
latency_ms?: number;
|
|
76
|
+
cache_hit?: boolean;
|
|
77
|
+
} | {
|
|
78
|
+
type: 'betterreviews.fetch.failure';
|
|
79
|
+
block_id?: string;
|
|
80
|
+
error_code: string;
|
|
81
|
+
retry_attempt?: number;
|
|
82
|
+
} | {
|
|
83
|
+
type: 'betterreviews.signature.invalid';
|
|
84
|
+
endpoint?: string;
|
|
85
|
+
source_ip_hash?: string;
|
|
86
|
+
} | {
|
|
87
|
+
type: 'betterreviews.schema.violation';
|
|
88
|
+
schema_version: number;
|
|
89
|
+
violation_path?: string;
|
|
90
|
+
dropped_count?: number;
|
|
91
|
+
} | {
|
|
92
|
+
type: 'betterreviews.webview.error';
|
|
93
|
+
error_code: string;
|
|
94
|
+
url_origin?: string;
|
|
95
|
+
};
|
|
96
|
+
type TelemetryHandler = (event: TelemetryEvent) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Returns a handler that swallows all events. Used as the default
|
|
99
|
+
* when the partner host app doesn't supply `onTelemetryEvent`.
|
|
100
|
+
*/
|
|
101
|
+
declare function noopTelemetry(): TelemetryHandler;
|
|
102
|
+
/**
|
|
103
|
+
* Wraps a handler so synchronous exceptions in the partner's
|
|
104
|
+
* implementation can't crash the renderer. Partner-side bugs become
|
|
105
|
+
* silent failures rather than UI outages.
|
|
106
|
+
*/
|
|
107
|
+
declare function safeTelemetry(handler: TelemetryHandler | undefined): TelemetryHandler;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Context provider for the BetterReviews RN package. Partner host
|
|
111
|
+
* apps wrap their navigation tree with `<BetterReviewsProvider>` and
|
|
112
|
+
* supply the merchant-resolved `theme` + `config` + an optional
|
|
113
|
+
* `onTelemetryEvent` callback.
|
|
114
|
+
*
|
|
115
|
+
* The provider is intentionally thin — no fetching, no state. The
|
|
116
|
+
* partner is responsible for fetching the metafield bodies from
|
|
117
|
+
* Shopify and passing them in. Decoupling fetch from render lets the
|
|
118
|
+
* partner cache however they want (subject to the 1h TTL ceiling per
|
|
119
|
+
* SECURITY.md § "Cache TTL contract").
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
interface BetterReviewsContextValue {
|
|
123
|
+
theme: Theme | null;
|
|
124
|
+
config: Config | null;
|
|
125
|
+
emit: TelemetryHandler;
|
|
126
|
+
}
|
|
127
|
+
interface BetterReviewsProviderProps {
|
|
128
|
+
theme?: Theme | null;
|
|
129
|
+
config?: Config | null;
|
|
130
|
+
onTelemetryEvent?: TelemetryHandler;
|
|
131
|
+
children: React.ReactNode;
|
|
132
|
+
}
|
|
133
|
+
declare function BetterReviewsProvider({ theme, config, onTelemetryEvent, children, }: BetterReviewsProviderProps): React.ReactElement;
|
|
134
|
+
declare function useBetterReviews(): BetterReviewsContextValue;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* `betterreviews_reactiv.product_content_block` (v1)
|
|
138
|
+
*
|
|
139
|
+
* Merchant-curated mobile PDP content. Resolved server-side by
|
|
140
|
+
* `PPO.Reactiv.ContentResolver`; RN reads exactly what merchant intent
|
|
141
|
+
* resolved to, no client-side fallback.
|
|
142
|
+
*
|
|
143
|
+
* Schema SHAPE-OF-TRUTH: this file matches what
|
|
144
|
+
* `lib/ppo/reactiv/content_resolver.ex` emits today. The
|
|
145
|
+
* `docs/proposals/reactiv-schema-2026-05-20.md` § 10 sketch used
|
|
146
|
+
* `kind` + a `show` boolean for `reviews_summary`; the implemented
|
|
147
|
+
* resolver uses `type` + presence-as-toggle. The implemented form is
|
|
148
|
+
* the production contract (PR #88 has been on `dev` since 2026-05-22)
|
|
149
|
+
* — proposal-vs-impl drift documented in
|
|
150
|
+
* `schemas/betterreviews-reactiv/DESIGN.md` § "Schema vs proposal doc".
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
declare const productContentBlockSchema: v.ObjectSchema<{
|
|
154
|
+
readonly v: v.LiteralSchema<1, undefined>;
|
|
155
|
+
readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, undefined>]>;
|
|
156
|
+
readonly sections: v.ArraySchema<v.VariantSchema<"type", [v.ObjectSchema<{
|
|
157
|
+
readonly type: v.LiteralSchema<"features", undefined>;
|
|
158
|
+
readonly bullets: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MaxLengthAction<string, 80, undefined>]>, undefined>, v.MaxLengthAction<string[], 6, undefined>]>;
|
|
159
|
+
}, undefined>, v.ObjectSchema<{
|
|
160
|
+
readonly type: v.LiteralSchema<"reviews_summary", undefined>;
|
|
161
|
+
}, undefined>], undefined>, undefined>;
|
|
162
|
+
}, undefined>;
|
|
163
|
+
type ProductContentBlock$1 = v.InferOutput<typeof productContentBlockSchema>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Top-level renderer for `betterreviews_reactiv.product_content_block`.
|
|
167
|
+
* Iterates the merchant's section list and dispatches each to its
|
|
168
|
+
* type-specific renderer. Unknown section types are silently dropped
|
|
169
|
+
* (tolerant-reader contract) with a `schema.violation` telemetry
|
|
170
|
+
* event.
|
|
171
|
+
*
|
|
172
|
+
* Mount-time gates:
|
|
173
|
+
* 1. `config.product_content_block_enabled === false` — render nothing.
|
|
174
|
+
* 2. `config.min_sdk_version` declared and current SDK is below
|
|
175
|
+
* floor — render nothing + emit `fetch.failure` with
|
|
176
|
+
* `error_code: "sdk_below_floor"`.
|
|
177
|
+
*
|
|
178
|
+
* Per-section validation uses valibot's `safeParse` so a single
|
|
179
|
+
* malformed section doesn't crash the whole block — bad sections
|
|
180
|
+
* are dropped, known-good ones render.
|
|
181
|
+
*
|
|
182
|
+
* Telemetry discipline: parsing + section building happen purely during
|
|
183
|
+
* render (no side effects); every telemetry event is emitted from a
|
|
184
|
+
* `useEffect`. Emitting during render would invoke the host app's
|
|
185
|
+
* `onTelemetryEvent` (commonly a `setState`) mid-render and crash React
|
|
186
|
+
* with "Cannot update a component while rendering a different component."
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
interface ProductContentBlockProps {
|
|
190
|
+
/** Parsed metafield body from `betterreviews_reactiv.product_content_block`. */
|
|
191
|
+
block: ProductContentBlock$1 | null | undefined;
|
|
192
|
+
}
|
|
193
|
+
declare function ProductContentBlock({ block }: ProductContentBlockProps): React.ReactElement | null;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Renders the `features` section — a bullet list capped by the schema
|
|
197
|
+
* at 6 bullets / 80 chars each. The schema's strict-overlay validator
|
|
198
|
+
* on the Elixir side has already enforced those caps; this component
|
|
199
|
+
* renders defensively in case a partial fallback path slips through.
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
interface FeaturesSectionProps {
|
|
203
|
+
bullets: string[];
|
|
204
|
+
}
|
|
205
|
+
declare function FeaturesSection({ bullets }: FeaturesSectionProps): React.ReactElement;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Renders the `reviews_summary` section. v1 displays a placeholder
|
|
209
|
+
* — the partner host app is expected to supply the live aggregate
|
|
210
|
+
* (star rating, count, top quote) by mounting its own review-summary
|
|
211
|
+
* component as a child or sibling.
|
|
212
|
+
*
|
|
213
|
+
* The presence of this section in the metafield blob is the merchant's
|
|
214
|
+
* "show me the reviews summary on this product" toggle. Absence means
|
|
215
|
+
* "don't render this section." A future PR may expand this to render
|
|
216
|
+
* `betterreviews.summary` data directly when supplied via
|
|
217
|
+
* `BetterReviewsProvider`'s `summary` prop.
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
declare function ReviewsSummarySection(): React.ReactElement;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* `WebViewHost` — the ONLY place `react-native-webview` is consumed.
|
|
224
|
+
*
|
|
225
|
+
* Critical seam discipline: this wrapper hides all underlying
|
|
226
|
+
* `WebView` props from callers. Public API is `{ url, onMessage,
|
|
227
|
+
* onError }` only. Recovery patches (e.g. adding
|
|
228
|
+
* `onContentProcessDidTerminate` if the Tier 2 WebView test surfaces
|
|
229
|
+
* a WebSocket-survival failure) are single-file, single-line changes
|
|
230
|
+
* INSIDE this file — zero changes at any caller. See
|
|
231
|
+
* `docs/proposals/reactiv-webview-tier2-test-2026-05-18.md` § "Known
|
|
232
|
+
* traps" for the documented recovery paths.
|
|
233
|
+
*
|
|
234
|
+
* Default `WebView` props match the Tier 2 test's chosen baseline:
|
|
235
|
+
* - sharedCookiesEnabled=false (worst-case isolation; Reactiv may
|
|
236
|
+
* flip later)
|
|
237
|
+
* - automaticallyAdjustContentInsets=false
|
|
238
|
+
* - contentInsetAdjustmentBehavior="never"
|
|
239
|
+
* - keyboardDisplayRequiresUserAction=false
|
|
240
|
+
* - allowsInlineMediaPlayback
|
|
241
|
+
* - mediaPlaybackRequiresUserAction=false
|
|
242
|
+
*
|
|
243
|
+
* The baseline matches what the Tier 2 test validates — keeps any
|
|
244
|
+
* future test pass/fail directly applicable.
|
|
245
|
+
*/
|
|
246
|
+
|
|
247
|
+
type WebViewProps = React.ComponentProps<typeof WebView>;
|
|
248
|
+
type OnMessageHandler = NonNullable<WebViewProps['onMessage']>;
|
|
249
|
+
type OnErrorHandler = NonNullable<WebViewProps['onError']>;
|
|
250
|
+
interface WebViewHostProps {
|
|
251
|
+
/** Chat surface URL (e.g. `https://api.betterreviews.app/review/chat?...`). */
|
|
252
|
+
url: string;
|
|
253
|
+
/** Fires on every `postMessage` from inside the WebView (except the close signal — see `onClose`). */
|
|
254
|
+
onMessage?: OnMessageHandler;
|
|
255
|
+
/** Fires on navigation / load error. */
|
|
256
|
+
onError?: OnErrorHandler;
|
|
257
|
+
/**
|
|
258
|
+
* Fires when the embedded page asks the host to dismiss it by posting
|
|
259
|
+
* `{ "type": "close" }` (e.g. the chat's "Back to store" / done button).
|
|
260
|
+
* Wire this to close the modal/screen the WebView is in. Other messages
|
|
261
|
+
* still flow to `onMessage`.
|
|
262
|
+
*/
|
|
263
|
+
onClose?: () => void;
|
|
264
|
+
}
|
|
265
|
+
declare function WebViewHost({ url, onMessage, onError, onClose }: WebViewHostProps): React.ReactElement | null;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* `StarRating` — compact aggregate rating badge (stars + score + review
|
|
269
|
+
* count), the RN equivalent of the storefront `br-star-rating` block. It sits
|
|
270
|
+
* near the product title; tap it (via `onPress`) to jump to the reviews.
|
|
271
|
+
*
|
|
272
|
+
* The host supplies the aggregate (`average` + `total`) — typically from the
|
|
273
|
+
* `betterreviews.summary` metafield it already fetches. A badge must NOT
|
|
274
|
+
* trigger `ReviewWidget`'s multi-call summary synthesis.
|
|
275
|
+
*
|
|
276
|
+
* Theme (star color) comes from `<BetterReviewsProvider>` context, with a fixed
|
|
277
|
+
* gold fallback when used outside a provider.
|
|
278
|
+
*/
|
|
279
|
+
|
|
280
|
+
interface StarRatingProps {
|
|
281
|
+
/** Aggregate average rating, e.g. 4.5. */
|
|
282
|
+
average: number;
|
|
283
|
+
/** Total review count. */
|
|
284
|
+
total: number;
|
|
285
|
+
/** Star pixel size (default 16). */
|
|
286
|
+
size?: number;
|
|
287
|
+
/** Override the star fill color (defaults to the theme's star color). */
|
|
288
|
+
starColor?: string;
|
|
289
|
+
/** Tap handler — e.g. scroll to the ReviewWidget. Renders as a button when set. */
|
|
290
|
+
onPress?: () => void;
|
|
291
|
+
/** Render nothing when there are no reviews (default true). */
|
|
292
|
+
hideWhenEmpty?: boolean;
|
|
293
|
+
}
|
|
294
|
+
declare function StarRating({ average, total, size, starColor, onPress, hideWhenEmpty, }: StarRatingProps): React.ReactElement | null;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Public widget API responses — review list / detail / vote.
|
|
298
|
+
*
|
|
299
|
+
* Schema SHAPE-OF-TRUTH: matches `PPO.Reviews.Widget.to_widget_json/2`
|
|
300
|
+
* + `paginate_meta/3` in `elixir/lib/ppo/reviews/widget.ex` (the
|
|
301
|
+
* `/api/widget/{store}/...` endpoints served by `WidgetController`).
|
|
302
|
+
*
|
|
303
|
+
* UNLIKE the `betterreviews_reactiv.*` metafield schemas in this package,
|
|
304
|
+
* these describe LIVE API responses, not merchant-curated metafields — so
|
|
305
|
+
* there is NO `v: 1` / `updated_at` envelope, and they are deliberately
|
|
306
|
+
* EXCLUDED from `scripts/generate.ts` (the JSON-Schema drift gate is for the
|
|
307
|
+
* server-side metafield pre-write validator; the widget API contract is
|
|
308
|
+
* owned by the Elixir controller, not validated from a generated schema).
|
|
309
|
+
*
|
|
310
|
+
* Tolerant reader: plain `v.object` ignores unknown keys (the server emits
|
|
311
|
+
* extra fields like `date_iso`/`display_product_*` that older SDK builds
|
|
312
|
+
* simply skip). Nullable where the server can emit `nil`.
|
|
313
|
+
*/
|
|
314
|
+
|
|
315
|
+
declare const widgetMediaItemSchema: v.ObjectSchema<{
|
|
316
|
+
readonly type: v.StringSchema<undefined>;
|
|
317
|
+
readonly thumbnail: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
318
|
+
readonly full: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
319
|
+
readonly alt: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
320
|
+
}, undefined>;
|
|
321
|
+
declare const widgetMerchantReplySchema: v.ObjectSchema<{
|
|
322
|
+
readonly body: v.StringSchema<undefined>;
|
|
323
|
+
readonly author_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
324
|
+
readonly date: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
325
|
+
}, undefined>;
|
|
326
|
+
declare const widgetReviewSchema: v.ObjectSchema<{
|
|
327
|
+
readonly id: v.NumberSchema<undefined>;
|
|
328
|
+
readonly author: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
329
|
+
readonly rating: v.NumberSchema<undefined>;
|
|
330
|
+
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
331
|
+
readonly body: v.StringSchema<undefined>;
|
|
332
|
+
readonly body_truncated: v.BooleanSchema<undefined>;
|
|
333
|
+
readonly verified: v.BooleanSchema<undefined>;
|
|
334
|
+
readonly date: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
335
|
+
readonly date_iso: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
336
|
+
readonly helpful_count: v.NumberSchema<undefined>;
|
|
337
|
+
readonly unhelpful_count: v.NumberSchema<undefined>;
|
|
338
|
+
readonly media: v.ArraySchema<v.ObjectSchema<{
|
|
339
|
+
readonly type: v.StringSchema<undefined>;
|
|
340
|
+
readonly thumbnail: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
341
|
+
readonly full: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
342
|
+
readonly alt: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
343
|
+
}, undefined>, undefined>;
|
|
344
|
+
readonly tags: v.ArraySchema<v.StringSchema<undefined>, undefined>;
|
|
345
|
+
readonly merchant_reply: v.NullableSchema<v.ObjectSchema<{
|
|
346
|
+
readonly body: v.StringSchema<undefined>;
|
|
347
|
+
readonly author_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
348
|
+
readonly date: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
349
|
+
}, undefined>, undefined>;
|
|
350
|
+
readonly display_product_id: v.StringSchema<undefined>;
|
|
351
|
+
readonly display_product_title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
352
|
+
}, undefined>;
|
|
353
|
+
declare const widgetPaginationSchema: v.ObjectSchema<{
|
|
354
|
+
readonly page: v.NumberSchema<undefined>;
|
|
355
|
+
readonly per_page: v.NumberSchema<undefined>;
|
|
356
|
+
readonly total: v.NumberSchema<undefined>;
|
|
357
|
+
readonly total_pages: v.NumberSchema<undefined>;
|
|
358
|
+
readonly has_next: v.BooleanSchema<undefined>;
|
|
359
|
+
}, undefined>;
|
|
360
|
+
declare const widgetVoteCountsSchema: v.ObjectSchema<{
|
|
361
|
+
readonly helpful_count: v.NumberSchema<undefined>;
|
|
362
|
+
readonly unhelpful_count: v.NumberSchema<undefined>;
|
|
363
|
+
}, undefined>;
|
|
364
|
+
declare const widgetSortValueSchema: v.PicklistSchema<["most_relevant", "most_helpful", "newest", "highest", "lowest"], undefined>;
|
|
365
|
+
type WidgetMediaItem = v.InferOutput<typeof widgetMediaItemSchema>;
|
|
366
|
+
type WidgetMerchantReply = v.InferOutput<typeof widgetMerchantReplySchema>;
|
|
367
|
+
type WidgetReview = v.InferOutput<typeof widgetReviewSchema>;
|
|
368
|
+
type WidgetPagination = v.InferOutput<typeof widgetPaginationSchema>;
|
|
369
|
+
type WidgetVoteCounts = v.InferOutput<typeof widgetVoteCountsSchema>;
|
|
370
|
+
type WidgetSortValue = v.InferOutput<typeof widgetSortValueSchema>;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Synthesized review summary — NOT a server response.
|
|
374
|
+
*
|
|
375
|
+
* There is no summary endpoint on the widget API. The client computes this
|
|
376
|
+
* from 7 list calls (5 single-rating totals for the distribution, 1
|
|
377
|
+
* `media_only=true` for the photo count, 1 plain page for the verified
|
|
378
|
+
* count) — see `createBetterReviewsClient.fetchSummary`. This schema exists
|
|
379
|
+
* so the type re-exports symmetrically with the other widget types; it is
|
|
380
|
+
* NOT fed to `scripts/generate.ts`.
|
|
381
|
+
*/
|
|
382
|
+
|
|
383
|
+
declare const widgetSummarySchema: v.ObjectSchema<{
|
|
384
|
+
readonly total: v.NumberSchema<undefined>;
|
|
385
|
+
readonly average: v.NumberSchema<undefined>;
|
|
386
|
+
readonly breakdown: v.ArraySchema<v.NumberSchema<undefined>, undefined>;
|
|
387
|
+
readonly positivePct: v.NumberSchema<undefined>;
|
|
388
|
+
readonly photoCount: v.NumberSchema<undefined>;
|
|
389
|
+
readonly verifiedCount: v.NumberSchema<undefined>;
|
|
390
|
+
}, undefined>;
|
|
391
|
+
type WidgetSummary = v.InferOutput<typeof widgetSummarySchema>;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Client types for the public widget API.
|
|
395
|
+
*
|
|
396
|
+
* The package owns the UI + pagination/sort/filter state and assembles the
|
|
397
|
+
* logical requests; the HOST owns transport — it implements the `Fetcher`,
|
|
398
|
+
* which prepends the API base URL and injects the auth `token`. No token
|
|
399
|
+
* ever lives in this package or the RN binary.
|
|
400
|
+
*/
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* A logical request the host fetcher fulfils. The host MUST prepend the API
|
|
404
|
+
* base URL, inject the `token`, perform the request, throw on a non-2xx
|
|
405
|
+
* status, and resolve with the parsed JSON body. The host MUST NOT put the
|
|
406
|
+
* resolved URL or the token into any error it throws (the token rides in the
|
|
407
|
+
* query string — a logged URL/error leaks it).
|
|
408
|
+
*/
|
|
409
|
+
interface FetcherRequest {
|
|
410
|
+
/** Path relative to the API base, e.g. `/api/widget/{store}/products/{product}/reviews`. */
|
|
411
|
+
path: string;
|
|
412
|
+
/** Non-secret query params. The token is added by the host, never here. */
|
|
413
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
414
|
+
method?: 'GET' | 'POST';
|
|
415
|
+
/** JSON-serializable body (vote only). */
|
|
416
|
+
body?: unknown;
|
|
417
|
+
signal?: AbortSignal;
|
|
418
|
+
}
|
|
419
|
+
type Fetcher = (req: FetcherRequest) => Promise<unknown>;
|
|
420
|
+
type VoteType = 'helpful' | 'unhelpful';
|
|
421
|
+
/**
|
|
422
|
+
* Host-provided persistence for "has this user voted on this review".
|
|
423
|
+
* Stores only review id → vote direction (no PII, no token, no review
|
|
424
|
+
* content) — safe for unencrypted storage like AsyncStorage. Do NOT widen
|
|
425
|
+
* it to cache review payloads.
|
|
426
|
+
*/
|
|
427
|
+
interface VoteStateStore {
|
|
428
|
+
get(reviewId: number): VoteType | null | undefined;
|
|
429
|
+
set(reviewId: number, vote: VoteType | null): void;
|
|
430
|
+
}
|
|
431
|
+
interface ListParams {
|
|
432
|
+
page?: number;
|
|
433
|
+
perPage?: number;
|
|
434
|
+
sort?: WidgetSortValue;
|
|
435
|
+
rating?: 1 | 2 | 3 | 4 | 5 | null;
|
|
436
|
+
mediaOnly?: boolean;
|
|
437
|
+
search?: string;
|
|
438
|
+
}
|
|
439
|
+
interface ReviewListResult {
|
|
440
|
+
reviews: WidgetReview[];
|
|
441
|
+
pagination: WidgetPagination;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Reported once per list response when one or more rows fail schema
|
|
445
|
+
* validation. Carries paths + a count ONLY — never row contents (untrusted
|
|
446
|
+
* customer-authored review text must not reach telemetry).
|
|
447
|
+
*/
|
|
448
|
+
interface SchemaViolationInfo {
|
|
449
|
+
droppedCount: number;
|
|
450
|
+
firstBadPath: string;
|
|
451
|
+
}
|
|
452
|
+
interface BetterReviewsClientConfig {
|
|
453
|
+
fetcher: Fetcher;
|
|
454
|
+
storeId: string;
|
|
455
|
+
productId: string;
|
|
456
|
+
onSchemaViolation?: (info: SchemaViolationInfo) => void;
|
|
457
|
+
}
|
|
458
|
+
interface BetterReviewsClient {
|
|
459
|
+
listReviews(params?: ListParams, signal?: AbortSignal): Promise<ReviewListResult>;
|
|
460
|
+
/** Detail is scoped to the configured product (`product_id` is always sent — the A4 IDOR fix). */
|
|
461
|
+
getReviewDetail(reviewId: number, signal?: AbortSignal): Promise<WidgetReview>;
|
|
462
|
+
/** Returns updated counts, or `null` if the POST succeeded but the body was unparseable. Rejects on HTTP/network error. */
|
|
463
|
+
voteReview(reviewId: number, type: VoteType, undo: boolean, signal?: AbortSignal): Promise<WidgetVoteCounts | null>;
|
|
464
|
+
fetchSummary(signal?: AbortSignal): Promise<WidgetSummary>;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* `<ReviewWidget>` — the public, stateful review-browsing surface.
|
|
469
|
+
*
|
|
470
|
+
* Renders INLINE — it owns no scroll container, so it drops into a host's
|
|
471
|
+
* product-page `ScrollView` as one section (below the product info /
|
|
472
|
+
* `ProductContentBlock`) and scrolls with the page. Paging is a "Load more"
|
|
473
|
+
* button, so no virtualization is needed.
|
|
474
|
+
*
|
|
475
|
+
* Theme + telemetry flow from `<BetterReviewsProvider>` context (same
|
|
476
|
+
* convention as `ProductContentBlock`); transport/auth flow from the host via
|
|
477
|
+
* the injected `client` (or `fetcher` + ids). Schema violations are emitted as
|
|
478
|
+
* telemetry from the client's async callback (post-render, so the b9357c30
|
|
479
|
+
* "no emit during render" rule holds).
|
|
480
|
+
*/
|
|
481
|
+
|
|
482
|
+
interface ReviewWidgetProps {
|
|
483
|
+
/** Primary: a client built via `createBetterReviewsClient`. */
|
|
484
|
+
client?: BetterReviewsClient;
|
|
485
|
+
/** Convenience: supply transport + ids and the widget builds the client. */
|
|
486
|
+
fetcher?: Fetcher;
|
|
487
|
+
storeId?: string;
|
|
488
|
+
productId?: string;
|
|
489
|
+
/** Persist "already voted" across launches; defaults to in-memory. */
|
|
490
|
+
voteStateStore?: VoteStateStore;
|
|
491
|
+
/** Host-owned write-review action; the CTA hides if omitted. */
|
|
492
|
+
onWriteReview?: () => void;
|
|
493
|
+
initialSort?: WidgetSortValue;
|
|
494
|
+
}
|
|
495
|
+
declare function ReviewWidget(props: ReviewWidgetProps): React.ReactElement;
|
|
496
|
+
|
|
497
|
+
declare function createBetterReviewsClient(config: BetterReviewsClientConfig): BetterReviewsClient;
|
|
498
|
+
/**
|
|
499
|
+
* Default in-memory `VoteStateStore`. Survives only for the component's
|
|
500
|
+
* lifetime; hosts wanting "already voted" to persist across launches pass an
|
|
501
|
+
* AsyncStorage-backed implementation instead.
|
|
502
|
+
*/
|
|
503
|
+
declare function createMemoryVoteStore(): VoteStateStore;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Maps a `betterreviews_reactiv.theme` JSON blob to a flat StyleSheet
|
|
507
|
+
* record consumed by the section renderers. All fields are optional —
|
|
508
|
+
* absent fields fall back to RN's platform defaults.
|
|
509
|
+
*
|
|
510
|
+
* The shape matches `PPO.Reactiv.ContentResolver.build_theme/1` —
|
|
511
|
+
* flat color keys + corner_style enum + font_family enum.
|
|
512
|
+
*/
|
|
513
|
+
|
|
514
|
+
interface ResolvedTheme {
|
|
515
|
+
primaryColor?: string;
|
|
516
|
+
backgroundColor?: string;
|
|
517
|
+
textColor?: string;
|
|
518
|
+
accentColor?: string;
|
|
519
|
+
borderRadius?: number;
|
|
520
|
+
fontFamily?: string;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Pure function — no React, no platform calls. Maps a Theme blob to a
|
|
524
|
+
* ResolvedTheme suitable for spreading into style props.
|
|
525
|
+
*
|
|
526
|
+
* Unknown enum values for `corner_style` / `font_family` resolve to
|
|
527
|
+
* `undefined` (the schema validator at the write boundary already
|
|
528
|
+
* rejects unknown values, but defensive in case of a malformed
|
|
529
|
+
* payload landing in the read path).
|
|
530
|
+
*/
|
|
531
|
+
declare function applyTheme(theme: Theme | null | undefined): ResolvedTheme;
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Semver floor compare. The metafield's optional `config.min_sdk_version`
|
|
535
|
+
* declares the minimum SDK version the merchant expects. The RN
|
|
536
|
+
* package compares against its own version on mount; if below the
|
|
537
|
+
* floor, ProductContentBlock renders nothing (degrade-gracefully).
|
|
538
|
+
*
|
|
539
|
+
* v1 of the resolver does NOT emit `min_sdk_version` — this is
|
|
540
|
+
* forward-compat plumbing.
|
|
541
|
+
*/
|
|
542
|
+
declare const SDK_VERSION: string;
|
|
543
|
+
/**
|
|
544
|
+
* Returns true if the SDK's current version meets the configured
|
|
545
|
+
* floor, false otherwise. A null/undefined floor always passes (no
|
|
546
|
+
* floor declared). Malformed floor strings are treated as "no floor"
|
|
547
|
+
* — the package errs on the side of rendering rather than refusing.
|
|
548
|
+
*/
|
|
549
|
+
declare function meetsFloor(floor: string | undefined): boolean;
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Bridge config resolution (Card C.14).
|
|
553
|
+
*
|
|
554
|
+
* `config.bridge` declares the merchant's preference for native bridge
|
|
555
|
+
* surfaces (sort drawer, photo viewer):
|
|
556
|
+
* - `"off"` (default): everything renders in-WebView.
|
|
557
|
+
* - `"auto"`: use native if available, fall back to in-WebView.
|
|
558
|
+
* - `"required"`: use native; if not available, render nothing.
|
|
559
|
+
*
|
|
560
|
+
* Schema spec § 11.5 — v1 of this RN package ships in-WebView fallback
|
|
561
|
+
* only. The native bridge is post-soft-launch. So all three values
|
|
562
|
+
* resolve to `"in_webview"` behaviour today; `"auto"` and `"required"`
|
|
563
|
+
* additionally emit a `bridge_not_implemented` telemetry hint so the
|
|
564
|
+
* future merchant who configures non-default can be notified that
|
|
565
|
+
* their intent is not yet honoured.
|
|
566
|
+
*/
|
|
567
|
+
|
|
568
|
+
type ResolvedBridge = 'in_webview' | 'native';
|
|
569
|
+
/**
|
|
570
|
+
* Returns the bridge mode this package will actually use. v1 always
|
|
571
|
+
* returns `"in_webview"` regardless of the config preference, since the
|
|
572
|
+
* native bridge isn't implemented yet.
|
|
573
|
+
*
|
|
574
|
+
* Side-effect: if the config requested `"auto"` or `"required"`, emits
|
|
575
|
+
* a `betterreviews.fetch.failure` telemetry event with
|
|
576
|
+
* `error_code: "bridge_not_implemented"` so the partner observability
|
|
577
|
+
* stack can surface that the merchant's intent is not yet honoured.
|
|
578
|
+
*/
|
|
579
|
+
declare function resolveBridge(config: Config | null | undefined, emit: TelemetryHandler): ResolvedBridge;
|
|
580
|
+
|
|
581
|
+
export { type BetterReviewsClient, type BetterReviewsClientConfig, type BetterReviewsContextValue, BetterReviewsProvider, type BetterReviewsProviderProps, type Config, FeaturesSection, type FeaturesSectionProps, type Fetcher, type FetcherRequest, type ListParams, ProductContentBlock, type ProductContentBlockProps, type ProductContentBlock$1 as ProductContentBlockSchema, type ResolvedBridge, type ResolvedTheme, type ReviewListResult, ReviewWidget, type ReviewWidgetProps, ReviewsSummarySection, SDK_VERSION, type SchemaViolationInfo, StarRating, type StarRatingProps, type TelemetryEvent, type TelemetryHandler, type Theme, type VoteStateStore, type VoteType, WebViewHost, type WebViewHostProps, type WidgetMediaItem, type WidgetMerchantReply, type WidgetPagination, type WidgetReview, type WidgetSortValue, type WidgetSummary, type WidgetVoteCounts, applyTheme, createBetterReviewsClient, createMemoryVoteStore, meetsFloor, noopTelemetry, resolveBridge, safeTelemetry, useBetterReviews };
|