@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/LICENSE
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
BetterReviews Proprietary License (BRPL) v1
|
|
2
|
+
Copyright (c) 2026 BetterReviews ("Licensor")
|
|
3
|
+
|
|
4
|
+
1. DEFINITIONS
|
|
5
|
+
|
|
6
|
+
"Software" means the `@betterreviews/react-native` npm package, including
|
|
7
|
+
its source code, generated artifacts, type definitions, and accompanying
|
|
8
|
+
documentation.
|
|
9
|
+
|
|
10
|
+
"Authorized Partner" means an entity that has executed a written
|
|
11
|
+
integration agreement with Licensor that explicitly names this Software
|
|
12
|
+
and grants right of use. As of the date of this LICENSE, the sole
|
|
13
|
+
Authorized Partner is Reactiv ("Reactiv Technologies Ltd." or its
|
|
14
|
+
successor entity).
|
|
15
|
+
|
|
16
|
+
"Production Use" means embedding or executing the Software in any
|
|
17
|
+
application distributed to end users, whether commercial or non-
|
|
18
|
+
commercial, paid or free.
|
|
19
|
+
|
|
20
|
+
"Evaluation Use" means executing the Software (a) against the BetterReviews
|
|
21
|
+
public mock endpoint with synthetic data only, or (b) against a
|
|
22
|
+
Licensor-issued sandbox credential, in either case strictly for the
|
|
23
|
+
purpose of evaluating the Software for a future integration agreement.
|
|
24
|
+
|
|
25
|
+
2. GRANT OF LICENSE
|
|
26
|
+
|
|
27
|
+
2.1. To Authorized Partners. Subject to the terms of this LICENSE and
|
|
28
|
+
the partner's written integration agreement, Licensor grants each
|
|
29
|
+
Authorized Partner a non-exclusive, non-transferable, non-sublicensable,
|
|
30
|
+
revocable license to install, execute, and embed the Software for
|
|
31
|
+
Production Use solely within the scope defined in that agreement.
|
|
32
|
+
|
|
33
|
+
2.2. To Evaluators. Licensor grants any entity a non-exclusive,
|
|
34
|
+
non-transferable, non-sublicensable, revocable license to install and
|
|
35
|
+
execute the Software for Evaluation Use only. Evaluation Use does not
|
|
36
|
+
authorize Production Use, redistribution, modification, or any use
|
|
37
|
+
beyond pre-integration evaluation.
|
|
38
|
+
|
|
39
|
+
3. RESTRICTIONS
|
|
40
|
+
|
|
41
|
+
You may not, and shall not permit any third party to:
|
|
42
|
+
|
|
43
|
+
(a) redistribute, sublicense, sell, lend, rent, or otherwise transfer
|
|
44
|
+
the Software, in source or compiled form, to any party other than
|
|
45
|
+
end users of an Authorized Partner's application;
|
|
46
|
+
|
|
47
|
+
(b) modify, adapt, translate, or create derivative works of the
|
|
48
|
+
Software, except for the minimum integration code required to wire
|
|
49
|
+
the Software into the Authorized Partner's application, and except
|
|
50
|
+
for bug fixes contributed back to Licensor under section 5;
|
|
51
|
+
|
|
52
|
+
(c) reverse engineer, decompile, or disassemble the Software, except
|
|
53
|
+
to the extent that applicable law expressly permits such activity
|
|
54
|
+
notwithstanding this restriction;
|
|
55
|
+
|
|
56
|
+
(d) remove, alter, or obscure any copyright, trademark, license, or
|
|
57
|
+
attribution notices contained in the Software;
|
|
58
|
+
|
|
59
|
+
(e) use the Software to build, train, or evaluate a product that
|
|
60
|
+
competes with the BetterReviews review-collection or review-display
|
|
61
|
+
platform;
|
|
62
|
+
|
|
63
|
+
(f) use the Software in any manner that would cause the Software to
|
|
64
|
+
become subject to an open-source license that requires disclosure
|
|
65
|
+
of any derivative work;
|
|
66
|
+
|
|
67
|
+
(g) extend Production Use beyond the scope granted in the partner's
|
|
68
|
+
written integration agreement.
|
|
69
|
+
|
|
70
|
+
4. OWNERSHIP
|
|
71
|
+
|
|
72
|
+
The Software is licensed, not sold. Licensor retains all right, title,
|
|
73
|
+
and interest in and to the Software, including all intellectual
|
|
74
|
+
property rights. No rights are granted to any trademark, service mark,
|
|
75
|
+
or trade name of Licensor except as expressly stated in a separate
|
|
76
|
+
written agreement.
|
|
77
|
+
|
|
78
|
+
5. CONTRIBUTIONS
|
|
79
|
+
|
|
80
|
+
If you submit a bug report, feature request, patch, or other
|
|
81
|
+
contribution to the Software, you grant Licensor a worldwide,
|
|
82
|
+
perpetual, irrevocable, royalty-free, sublicensable license to use,
|
|
83
|
+
modify, distribute, and incorporate that contribution into the
|
|
84
|
+
Software under any terms Licensor chooses, including under this
|
|
85
|
+
LICENSE.
|
|
86
|
+
|
|
87
|
+
6. TERMINATION
|
|
88
|
+
|
|
89
|
+
6.1. This LICENSE terminates automatically upon (a) any material
|
|
90
|
+
breach by you of these terms, or (b) termination or expiration of
|
|
91
|
+
the underlying written integration agreement, whichever occurs first.
|
|
92
|
+
|
|
93
|
+
6.2. Licensor may terminate this LICENSE for any reason with thirty
|
|
94
|
+
(30) days' written notice. On termination, you must cease all use
|
|
95
|
+
of the Software and remove the Software from any application under
|
|
96
|
+
your control within thirty (30) days.
|
|
97
|
+
|
|
98
|
+
6.3. Sections 3, 4, 5, 7, 8, and 9 survive termination.
|
|
99
|
+
|
|
100
|
+
7. CONFIDENTIALITY
|
|
101
|
+
|
|
102
|
+
The Software and any non-public information about its internal
|
|
103
|
+
structure, behavior, or roadmap is confidential to Licensor. You
|
|
104
|
+
shall protect such information with at least the same degree of care
|
|
105
|
+
you use for your own confidential information of similar
|
|
106
|
+
sensitivity, and in no event with less than reasonable care. This
|
|
107
|
+
obligation does not apply to information that is or becomes
|
|
108
|
+
publicly available through no fault of yours.
|
|
109
|
+
|
|
110
|
+
8. WARRANTY DISCLAIMER
|
|
111
|
+
|
|
112
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND "AS AVAILABLE," WITHOUT WARRANTY
|
|
113
|
+
OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
114
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
115
|
+
NON-INFRINGEMENT, AND ACCURACY OF DATA. LICENSOR DOES NOT WARRANT
|
|
116
|
+
THAT THE SOFTWARE WILL OPERATE UNINTERRUPTED OR ERROR-FREE.
|
|
117
|
+
|
|
118
|
+
9. LIMITATION OF LIABILITY
|
|
119
|
+
|
|
120
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL
|
|
121
|
+
LICENSOR BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
|
|
122
|
+
CONSEQUENTIAL, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS, REVENUE,
|
|
123
|
+
DATA, USE, OR GOODWILL, ARISING OUT OF OR IN CONNECTION WITH THIS
|
|
124
|
+
LICENSE OR THE SOFTWARE, WHETHER BASED IN CONTRACT, TORT (INCLUDING
|
|
125
|
+
NEGLIGENCE), STRICT LIABILITY, OR ANY OTHER THEORY, EVEN IF
|
|
126
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. LICENSOR'S TOTAL
|
|
127
|
+
AGGREGATE LIABILITY UNDER THIS LICENSE SHALL NOT EXCEED ONE
|
|
128
|
+
HUNDRED U.S. DOLLARS ($100).
|
|
129
|
+
|
|
130
|
+
10. GOVERNING LAW
|
|
131
|
+
|
|
132
|
+
This LICENSE is governed by the laws of England and Wales, without
|
|
133
|
+
regard to its conflict-of-laws principles. The courts located in
|
|
134
|
+
London, England shall have exclusive jurisdiction over any dispute
|
|
135
|
+
arising out of or relating to this LICENSE.
|
|
136
|
+
|
|
137
|
+
11. ENTIRE AGREEMENT
|
|
138
|
+
|
|
139
|
+
This LICENSE, together with any executed written integration
|
|
140
|
+
agreement between you and Licensor, constitutes the entire
|
|
141
|
+
agreement between the parties with respect to the Software and
|
|
142
|
+
supersedes all prior or contemporaneous oral or written
|
|
143
|
+
communications, proposals, and representations.
|
|
144
|
+
|
|
145
|
+
For licensing inquiries, contact: legal@betterreviews.app
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# @betterreviews/react-native
|
|
2
|
+
|
|
3
|
+
Native React Native renderer for BetterReviews mobile PDP content. Consumes the `betterreviews_reactiv.*` Shopify metafield namespace and renders product content blocks themed to the merchant.
|
|
4
|
+
|
|
5
|
+
## Authorized Partner
|
|
6
|
+
|
|
7
|
+
This package is licensed for use by **Reactiv** under a written integration agreement with BetterReviews. See [LICENSE](./LICENSE).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
yarn add @betterreviews/react-native \
|
|
13
|
+
react-native-webview react-native-svg react-native-gesture-handler
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Peer dependencies your host app must provide: `react ≥18`, `react-native ≥0.74`, `react-native-webview ≥13`, `react-native-svg ≥15`, and `react-native-gesture-handler ≥2.16`. `valibot` is bundled as a direct dependency — you don't install it.
|
|
17
|
+
|
|
18
|
+
**`react-native-gesture-handler` setup (required by `ReviewWidget`'s media viewer):** import it as the **very first line** of your app entry (`index.js`/`index.ts`) and wrap your app root in `GestureHandlerRootView`:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import 'react-native-gesture-handler'; // must be first
|
|
22
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
23
|
+
|
|
24
|
+
export default function Root() {
|
|
25
|
+
return <GestureHandlerRootView style={{ flex: 1 }}>{/* app */}</GestureHandlerRootView>;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **npm users:** React Native 0.74 ships a mismatched `@types/react` peer range, so a plain `npm install` may fail with `ERESOLVE`. This is an upstream React Native quirk, not specific to this package — install with `npm install --legacy-peer-deps`, or use Yarn (which is more lenient). Yarn is recommended for React Native projects.
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import {
|
|
35
|
+
BetterReviewsProvider,
|
|
36
|
+
ProductContentBlock,
|
|
37
|
+
type Theme,
|
|
38
|
+
type Config,
|
|
39
|
+
type ProductContentBlockSchema,
|
|
40
|
+
} from '@betterreviews/react-native';
|
|
41
|
+
|
|
42
|
+
function App() {
|
|
43
|
+
// Partner host app fetches the three metafield bodies from Shopify
|
|
44
|
+
// (storefront API or partner-backend proxy) for the active product.
|
|
45
|
+
const theme: Theme | null = useFetchedTheme(productId);
|
|
46
|
+
const config: Config | null = useFetchedConfig(productId);
|
|
47
|
+
const block: ProductContentBlockSchema | null = useFetchedBlock(productId);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<BetterReviewsProvider
|
|
51
|
+
theme={theme}
|
|
52
|
+
config={config}
|
|
53
|
+
onTelemetryEvent={(event) => {
|
|
54
|
+
// Forward to partner's own observability stack.
|
|
55
|
+
partnerAnalytics.log(event);
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<ProductContentBlock block={block} />
|
|
59
|
+
</BetterReviewsProvider>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Render gates
|
|
65
|
+
|
|
66
|
+
`<ProductContentBlock>` renders nothing (returns `null`) when any of these are true:
|
|
67
|
+
|
|
68
|
+
- `config.product_content_block_enabled === false` — merchant explicitly disabled this product
|
|
69
|
+
- `config.min_sdk_version` declared and current SDK below floor — emits `betterreviews.fetch.failure` telemetry
|
|
70
|
+
- `block` is `null` / `undefined`
|
|
71
|
+
- The block envelope fails top-level schema validation — emits `betterreviews.schema.violation` telemetry
|
|
72
|
+
|
|
73
|
+
Per-section validation uses tolerant-reader semantics: an individual malformed section is dropped (with telemetry) while the rest render.
|
|
74
|
+
|
|
75
|
+
## Telemetry events
|
|
76
|
+
|
|
77
|
+
| Event | When |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `betterreviews.fetch.failure` | Mount blocked by `min_sdk_version` floor (`error_code: "sdk_below_floor"`) |
|
|
80
|
+
| `betterreviews.schema.violation` | Envelope or per-section validation failed |
|
|
81
|
+
|
|
82
|
+
Future versions will emit `betterreviews.fetch.success`, `betterreviews.signature.invalid`, and `betterreviews.webview.error` (the last lands with the WebView host in Card C.12b).
|
|
83
|
+
|
|
84
|
+
## Theming
|
|
85
|
+
|
|
86
|
+
The widgets are **neutral by default** — a black CTA on zinc grayscale. No brand color appears unless the host opts in by passing a `theme` to `<BetterReviewsProvider>` (`background_color`, `text_color`, `accent_color`, `corner_style`, `font_family`). The only non-grayscale defaults are the **gold stars/bars** and the green **"Verified Buyer"** badge (universal review conventions, fixed in v1).
|
|
87
|
+
|
|
88
|
+
## StarRating (aggregate badge)
|
|
89
|
+
|
|
90
|
+
`<StarRating>` is the compact rating badge (stars + score + review count) for near the product title — the RN equivalent of the storefront `br-star-rating` block. The host supplies the aggregate (`average` + `total`, typically from the `betterreviews.summary` metafield it already fetches); the badge does **not** call the API.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { StarRating } from '@betterreviews/react-native';
|
|
94
|
+
|
|
95
|
+
<StarRating average={4.5} total={128} onPress={scrollToReviews} />
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Star color comes from `<BetterReviewsProvider>` theme (gold fallback outside a provider); `onPress` makes it a button (e.g. scroll to the `ReviewWidget`); it renders nothing when `total` is 0 (`hideWhenEmpty`, default on).
|
|
99
|
+
|
|
100
|
+
## ReviewWidget (review browsing + voting)
|
|
101
|
+
|
|
102
|
+
`<ReviewWidget>` renders the full review-browsing surface — paginated list, rating/photo/search filters, sort, read-more, a full-screen media viewer, and helpful/unhelpful voting.
|
|
103
|
+
|
|
104
|
+
The package owns the UI + pagination/sort/filter state. **Your host app owns transport and auth** via an injected `Fetcher`: it prepends the API base URL, injects the widget `token`, and returns parsed JSON (throwing on a non-2xx status). No token ever lives inside this package or your app binary's SDK code — keep it in your host's secure config / backend proxy.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import {
|
|
108
|
+
ReviewWidget,
|
|
109
|
+
createBetterReviewsClient,
|
|
110
|
+
type Fetcher,
|
|
111
|
+
} from '@betterreviews/react-native';
|
|
112
|
+
|
|
113
|
+
const fetcher: Fetcher = async ({ path, query, method = 'GET', body, signal }) => {
|
|
114
|
+
const params = new URLSearchParams();
|
|
115
|
+
for (const [k, v] of Object.entries(query ?? {})) if (v !== undefined) params.set(k, String(v));
|
|
116
|
+
params.set('token', getWidgetTokenFromSecureConfig()); // host-injected; never hardcode
|
|
117
|
+
const res = await fetch(`${API_BASE}${path}?${params}`, {
|
|
118
|
+
method, signal,
|
|
119
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
120
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok) throw new Error(`widget request failed: HTTP ${res.status}`); // never log the URL — the token is in it
|
|
123
|
+
return res.json();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const client = createBetterReviewsClient({ fetcher, storeId, productId });
|
|
127
|
+
|
|
128
|
+
// Theme + telemetry come from <BetterReviewsProvider> context (NOT props).
|
|
129
|
+
<BetterReviewsProvider theme={theme} onTelemetryEvent={partnerAnalytics.log}>
|
|
130
|
+
<ReviewWidget client={client} onWriteReview={openReviewChat} />
|
|
131
|
+
</BetterReviewsProvider>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Notes:
|
|
135
|
+
- **`ReviewWidget` renders inline** — it owns no scroll container, so drop it into your product-page `ScrollView` as one section (e.g. below the product info and `ProductContentBlock`) and it scrolls with the page. Paging is a "Load more" button (no virtualization). The sort drawer and full-screen media viewer are `Modal` overlays, so they work regardless.
|
|
136
|
+
- `onWriteReview` is host-owned (open your chat WebView / nav). The CTA hides if omitted. Never pass server-controlled strings to `Linking.openURL`.
|
|
137
|
+
- Voting persists in-memory by default; pass `voteStateStore` (e.g. AsyncStorage-backed) to persist "already voted" across launches. Store only review id → direction; never review content or the token.
|
|
138
|
+
- The widget requires the `GestureHandlerRootView` root wrap above.
|
|
139
|
+
|
|
140
|
+
## Security obligations on the host
|
|
141
|
+
|
|
142
|
+
See [SECURITY.md § "What you must do (the host)"](./SECURITY.md) for the contract every embedding host app must honor — credential storage, GDPR cascade, Shopify Level 2 data scope, cache TTL ceiling, Info.plist permissions, logging discipline.
|
|
143
|
+
|
|
144
|
+
## Versioning
|
|
145
|
+
|
|
146
|
+
This package follows semver. The `betterreviews_reactiv.*` metafield schema (generated JSON Schemas at `elixir/priv/reactiv_schemas/v1/`) follows additive-only compatibility with a 90-day deprecation window — see `schemas/betterreviews-reactiv/COMPATIBILITY.md` at the BetterReviews repo root.
|
|
147
|
+
|
|
148
|
+
## WebView surface
|
|
149
|
+
|
|
150
|
+
For the customer-facing chat flow (`/review/chat`), use `WebViewHost`:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { WebViewHost } from '@betterreviews/react-native';
|
|
154
|
+
|
|
155
|
+
<WebViewHost
|
|
156
|
+
url={`https://api.betterreviews.app/review/chat?store_id=${storeId}&product_id=${productId}&token=${token}`}
|
|
157
|
+
onMessage={(event) => console.log('message from chat', event.nativeEvent.data)}
|
|
158
|
+
onError={(event) => console.warn('webview error', event.nativeEvent)}
|
|
159
|
+
onClose={() => dismissChat()} // fires when the page posts {type:'close'} (e.g. "Back to store")
|
|
160
|
+
/>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The component intentionally exposes only `{ url, onMessage, onError }`. All underlying `WebView` props (cookie scope, content-inset behavior, keyboard handling, media playback) are locked to the baseline validated by the Tier 2 WebView test (`docs/proposals/reactiv-webview-tier2-test-2026-05-18.md`). Future recovery patches stay surgical.
|
|
164
|
+
|
|
165
|
+
`react-native-webview ≥13.0.0` is a peer dep. Add it to your host app:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
yarn add react-native-webview
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Bridge config
|
|
172
|
+
|
|
173
|
+
`config.bridge` declares the merchant's preference for native bridge surfaces:
|
|
174
|
+
|
|
175
|
+
- `"off"` (default): everything renders in-WebView.
|
|
176
|
+
- `"auto"`: native if available, fall back to in-WebView.
|
|
177
|
+
- `"required"`: native; if not available, render nothing.
|
|
178
|
+
|
|
179
|
+
v1 of this package supports `"off"` only. `"auto"` and `"required"` resolve to `"off"` behavior and emit a `betterreviews.fetch.failure` telemetry event with `error_code: "bridge_not_implemented"` so partner observability can surface the unhonored intent. Native bridge implementations land post-soft-launch (Card C.14).
|
|
180
|
+
|
|
181
|
+
## Not yet shipped
|
|
182
|
+
|
|
183
|
+
- Native (non-WebView) bridge surfaces for the chat flow (Card C.14 — post-soft-launch)
|
|
184
|
+
- Per-surface theming of the widget's fixed neutrals (star/verified/muted/border/scrim) — a later additive `theme` schema bump
|
|
185
|
+
- HMAC signature verification (forward-compat — added when a bidirectional channel emerges)
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
Proprietary. See [LICENSE](./LICENSE). For licensing inquiries: `legal@betterreviews.app`.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Email `security@betterreviews.app` with a description of the issue, steps
|
|
6
|
+
to reproduce, and any proof-of-concept code. Encrypt sensitive details
|
|
7
|
+
with our PGP key if you prefer (fingerprint: PENDING — published at
|
|
8
|
+
https://betterreviews.app/.well-known/security.txt).
|
|
9
|
+
|
|
10
|
+
**Do not file a public GitHub issue or open a PR for security issues.**
|
|
11
|
+
|
|
12
|
+
We commit to:
|
|
13
|
+
|
|
14
|
+
- Acknowledge receipt within 24 hours.
|
|
15
|
+
- Provide an initial assessment within 72 hours.
|
|
16
|
+
- Coordinate disclosure timing with you. We aim to patch within 14 days
|
|
17
|
+
for critical issues, 30 days for high, 90 days for medium and low.
|
|
18
|
+
- Credit you in our advisory if you wish, after the fix ships.
|
|
19
|
+
|
|
20
|
+
We will not pursue legal action against good-faith security research
|
|
21
|
+
that follows this policy. Specifically, we permit:
|
|
22
|
+
|
|
23
|
+
- Testing against the BetterReviews public mock endpoint
|
|
24
|
+
(`https://api.betterreviews.app/api/v1/reactiv/mock/*`) with synthetic
|
|
25
|
+
data only.
|
|
26
|
+
- Testing against your own sandbox credential.
|
|
27
|
+
- Decompiling or reverse-engineering this package for the sole purpose
|
|
28
|
+
of identifying a vulnerability you intend to report under this policy.
|
|
29
|
+
|
|
30
|
+
We do not permit testing that:
|
|
31
|
+
|
|
32
|
+
- Uses real production credentials issued to a different partner.
|
|
33
|
+
- Accesses, modifies, or attempts to access another merchant's data.
|
|
34
|
+
- Causes service disruption (DoS, fuzzing at scale, etc.).
|
|
35
|
+
- Persists data exfiltrated from BetterReviews beyond what is needed
|
|
36
|
+
to demonstrate the issue.
|
|
37
|
+
|
|
38
|
+
## Supported versions
|
|
39
|
+
|
|
40
|
+
| Version | Supported |
|
|
41
|
+
|---------|---------------|
|
|
42
|
+
| 1.x | Yes |
|
|
43
|
+
| < 1.0 | Pre-release — no security commitments. Upgrade. |
|
|
44
|
+
|
|
45
|
+
When a v2 ships, v1 remains supported with security patches for 12
|
|
46
|
+
months from the v2 release date.
|
|
47
|
+
|
|
48
|
+
## What BetterReviews commits to (the wire)
|
|
49
|
+
|
|
50
|
+
The wire between this package and `api.betterreviews.app` is designed
|
|
51
|
+
fail-closed. Specifically:
|
|
52
|
+
|
|
53
|
+
- **HMAC signatures.** Every outbound state-change webhook from
|
|
54
|
+
BetterReviews to a partner endpoint is signed HMAC-SHA256. The
|
|
55
|
+
package verifies signatures using `Plug.Crypto.secure_compare`-
|
|
56
|
+
equivalent constant-time comparison; a missing or invalid signature
|
|
57
|
+
rejects the message.
|
|
58
|
+
- **Per-merchant credentials.** The `reactiv_app_id` issued to a
|
|
59
|
+
partner is scoped per merchant. A leaked credential exposes one
|
|
60
|
+
merchant's content stream, not all merchants.
|
|
61
|
+
- **No PII at rest in the package.** The package does not persist
|
|
62
|
+
customer email, name, IP, or any conversation transcript locally.
|
|
63
|
+
Review content fetched from BetterReviews is rendered and discarded
|
|
64
|
+
on view-change. If your host app caches review content, that is your
|
|
65
|
+
responsibility under the BR data-handling addendum.
|
|
66
|
+
- **Tolerant-reader pattern.** Unknown JSON fields are ignored, never
|
|
67
|
+
echoed back to BR or forwarded to logs. Unknown section types in
|
|
68
|
+
product content blocks are skipped, logged locally, and reported via
|
|
69
|
+
the telemetry hook (see "Observability" below).
|
|
70
|
+
- **WKWebView origin pinning.** The package's WebView host for
|
|
71
|
+
`/review/chat` pins the origin to `*.betterreviews.app` and
|
|
72
|
+
`*.betterreviews.ngrok-free.dev` (development only). Cross-origin
|
|
73
|
+
navigations are blocked.
|
|
74
|
+
- **Schema validation.** Content blocks are validated against the
|
|
75
|
+
generated JSON Schema at `elixir/priv/reactiv_schemas/v1/` before
|
|
76
|
+
render. Schema mismatches are dropped, not partially rendered.
|
|
77
|
+
- **Storage of partner credentials.** BetterReviews stores partner
|
|
78
|
+
credentials encrypted at rest via `PiiVault` (AES-GCM with per-tenant
|
|
79
|
+
key derivation). Plaintext credentials never appear in logs, audit
|
|
80
|
+
rows, or error messages.
|
|
81
|
+
|
|
82
|
+
## What you must do (the host)
|
|
83
|
+
|
|
84
|
+
Integrating this package into your host app shifts certain obligations
|
|
85
|
+
to you. The integration is not safe unless you do these:
|
|
86
|
+
|
|
87
|
+
### 1. Store credentials in iOS Keychain / Android Keystore
|
|
88
|
+
|
|
89
|
+
The `reactiv_app_id` and any merchant-specific tokens must be stored
|
|
90
|
+
in the platform-native secure enclave, never in `AsyncStorage`,
|
|
91
|
+
`UserDefaults`, or shared preferences. Example: use
|
|
92
|
+
`react-native-keychain` with `ACCESSIBLE_WHEN_UNLOCKED_THIS_DEVICE_ONLY`.
|
|
93
|
+
|
|
94
|
+
### 2. Honor the GDPR cascade
|
|
95
|
+
|
|
96
|
+
When a customer requests deletion under GDPR Article 17, you MUST:
|
|
97
|
+
|
|
98
|
+
- Stop calling `/api/v1/reactiv/*` endpoints with that customer's
|
|
99
|
+
identifiers within 30 days of the request.
|
|
100
|
+
- Purge any locally cached review content tied to that customer within
|
|
101
|
+
the same window.
|
|
102
|
+
- Forward the request to BetterReviews via
|
|
103
|
+
`POST /api/v1/reactiv/customer-data-erasure` with the customer's
|
|
104
|
+
email hash (SHA-256 of lowercased email).
|
|
105
|
+
|
|
106
|
+
A separate Data Processing Addendum covers BetterReviews' obligations
|
|
107
|
+
on the upstream side.
|
|
108
|
+
|
|
109
|
+
### 3. Honor the Shopify Level 2 protected data scope
|
|
110
|
+
|
|
111
|
+
BetterReviews holds Shopify Level 2 protected customer data approval.
|
|
112
|
+
Any review content you render through this package may include data
|
|
113
|
+
classified as protected customer data under Shopify's policies. You
|
|
114
|
+
must ensure your host app's Shopify data-handling commitments cover
|
|
115
|
+
the data your app receives via this package, or terminate display when
|
|
116
|
+
the merchant's Shopify access is revoked.
|
|
117
|
+
|
|
118
|
+
### 4. Honor the cache TTL ceiling
|
|
119
|
+
|
|
120
|
+
Review content fetched from BetterReviews may not be cached longer
|
|
121
|
+
than 1 hour without re-fetch. This is enforced upstream by the
|
|
122
|
+
`Cache-Control: max-age=3600` header on every `/api/v1/reactiv/content`
|
|
123
|
+
response. The package respects this header on its own internal cache;
|
|
124
|
+
if your host app adds its own cache layer, it must respect the same
|
|
125
|
+
ceiling. (This bound is set by the BetterReviews GDPR remediation lag
|
|
126
|
+
requirement — see the schema doc § 11 gap 8.)
|
|
127
|
+
|
|
128
|
+
### 5. Configure Info.plist permissions
|
|
129
|
+
|
|
130
|
+
The chat surface uses the iOS native photo picker. Your host app's
|
|
131
|
+
`Info.plist` must declare:
|
|
132
|
+
|
|
133
|
+
- `NSCameraUsageDescription`
|
|
134
|
+
- `NSPhotoLibraryUsageDescription`
|
|
135
|
+
- `NSMicrophoneUsageDescription` (only if you enable the
|
|
136
|
+
voice-input experiment)
|
|
137
|
+
|
|
138
|
+
Without these, the photo step in the chat surface silently fails on
|
|
139
|
+
iOS.
|
|
140
|
+
|
|
141
|
+
### 6. Do not log review content, customer emails, or partner
|
|
142
|
+
credentials
|
|
143
|
+
|
|
144
|
+
The package emits structured telemetry events via the configured
|
|
145
|
+
telemetry hook (see "Observability"). Those events are pre-redacted.
|
|
146
|
+
If you add your own logs around the package, do not log raw event
|
|
147
|
+
payloads — they may contain review content (untrusted user input) or
|
|
148
|
+
customer identifiers.
|
|
149
|
+
|
|
150
|
+
### 7. Use mock-mode for development
|
|
151
|
+
|
|
152
|
+
Do not use a production partner credential during development. The
|
|
153
|
+
package supports a mock-mode flag that routes all calls to
|
|
154
|
+
`https://api.betterreviews.app/api/v1/reactiv/mock/*`, which returns
|
|
155
|
+
synthetic data and accepts any well-formed signature. Configure it
|
|
156
|
+
via the `BR_REACTIV_MOCK_MODE=true` environment variable, or
|
|
157
|
+
programmatically via the `mockMode: true` package option.
|
|
158
|
+
|
|
159
|
+
### 8. Widget read/vote fetcher surface (`ReviewWidget`)
|
|
160
|
+
|
|
161
|
+
`ReviewWidget` reads and votes via the public widget API (`/api/widget/...`),
|
|
162
|
+
authenticated by a widget `token` query param that **your injected `Fetcher`
|
|
163
|
+
supplies** — the token never lives in this package or your binary's SDK code.
|
|
164
|
+
Obligations:
|
|
165
|
+
|
|
166
|
+
- **Inject the token host-side, from secure config — never a `const TOKEN =
|
|
167
|
+
'...'` literal, never committed.** The widget read token is the public,
|
|
168
|
+
store-scoped HMAC token; it is rotated only by a global secret rotation that
|
|
169
|
+
affects every surface, so treat a leak as high-impact and keep it out of the
|
|
170
|
+
app binary (inject from your backend / secure store).
|
|
171
|
+
- **Never log the resolved request URL or the raw fetcher error.** The token
|
|
172
|
+
rides in the query string — a logged URL or an `Error` whose message includes
|
|
173
|
+
the URL leaks it. Throw a coarse message (`HTTP <status>`), not the URL.
|
|
174
|
+
- **Render review content (`body`/`title`/`author`/`tags`/`merchant_reply`) in
|
|
175
|
+
`<Text>` only — never a WebView, `react-native-render-html`, or any HTML
|
|
176
|
+
renderer.** Review text is untrusted, user-authored input; RN `<Text>` renders
|
|
177
|
+
it inert, an HTML renderer does not.
|
|
178
|
+
- **Do not pass server-controlled strings to `Linking.openURL`.** Your
|
|
179
|
+
`onWriteReview` handler is host-owned; keep it to your own URLs.
|
|
180
|
+
- The SDK already drops any media URL that is not `https://` and never sends
|
|
181
|
+
`product_id`-less detail/vote calls (the server scopes detail/vote to the
|
|
182
|
+
product). Don't bypass the client by reading the raw response shape yourself.
|
|
183
|
+
|
|
184
|
+
## Observability
|
|
185
|
+
|
|
186
|
+
The package emits the following telemetry event types via the
|
|
187
|
+
configured `onTelemetryEvent` hook. All events are pre-redacted; no
|
|
188
|
+
customer PII appears in any payload.
|
|
189
|
+
|
|
190
|
+
| Event | When | Payload (redacted) |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| `betterreviews.fetch.success` | Successful content fetch | `{block_id, latency_ms, cache_hit}` |
|
|
193
|
+
| `betterreviews.fetch.failure` | Failed content fetch | `{block_id, error_code, retry_attempt}` |
|
|
194
|
+
| `betterreviews.signature.invalid` | HMAC verification failed | `{endpoint, source_ip_hash}` |
|
|
195
|
+
| `betterreviews.schema.violation` | Content/response failed schema validation (one event per response — bad list rows are dropped, the rest render) | `{schema_version, violation_path, dropped_count}` |
|
|
196
|
+
| `betterreviews.webview.error` | WebView navigation error | `{error_code, url_origin}` |
|
|
197
|
+
|
|
198
|
+
Forward these events to your own observability stack. BetterReviews
|
|
199
|
+
also receives an aggregate signal via the heartbeat endpoint (no
|
|
200
|
+
per-event forwarding from your side).
|
|
201
|
+
|
|
202
|
+
## Cryptographic primitives
|
|
203
|
+
|
|
204
|
+
| Use | Primitive |
|
|
205
|
+
|---|---|
|
|
206
|
+
| Outbound webhook signing | HMAC-SHA256 |
|
|
207
|
+
| Constant-time signature compare | `Plug.Crypto.secure_compare` (Elixir side); `crypto.timingSafeEqual` (Node side); the package uses the platform-native equivalent on RN |
|
|
208
|
+
| TLS | TLS 1.3 required for all `*.betterreviews.app` endpoints |
|
|
209
|
+
| PII at rest | AES-256-GCM via PiiVault (server side); package does not persist PII |
|
|
210
|
+
| Email hash | SHA-256 of `lowercase(email)` |
|
|
211
|
+
|
|
212
|
+
## Out-of-scope assets
|
|
213
|
+
|
|
214
|
+
The following are intentionally not covered by this policy and should
|
|
215
|
+
not be the target of security research:
|
|
216
|
+
|
|
217
|
+
- Third-party services BetterReviews uses (Anthropic, OpenAI, AWS,
|
|
218
|
+
Cloudflare, Shopify). Report vulnerabilities in those to the vendor
|
|
219
|
+
directly.
|
|
220
|
+
- Sample apps, demo stores, or fixtures distributed alongside this
|
|
221
|
+
package. They exist to demonstrate integration patterns and may
|
|
222
|
+
contain intentionally simplified code.
|
|
223
|
+
- The mock endpoint's synthetic data. By design, it accepts any
|
|
224
|
+
well-formed signature so partner-side dev does not require real
|
|
225
|
+
credentials.
|
|
226
|
+
|
|
227
|
+
## Contact
|
|
228
|
+
|
|
229
|
+
- Vulnerability reports: `security@betterreviews.app`
|
|
230
|
+
- General partner security questions:
|
|
231
|
+
`partner-security@betterreviews.app`
|
|
232
|
+
- BetterReviews security lead: PENDING (publish name + LinkedIn once
|
|
233
|
+
team grows).
|
|
234
|
+
|
|
235
|
+
## Acknowledgements
|
|
236
|
+
|
|
237
|
+
We thank the following researchers for responsible disclosure
|
|
238
|
+
(populated post-launch).
|