@betterreviews/react-native
v1.0.0
Published
React Native renderer for BetterReviews mobile PDP content. Consumes the betterreviews_reactiv.* Shopify metafields and renders product content blocks (features, reviews_summary) themed to the merchant.
Readme
@betterreviews/react-native
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.
Authorized Partner
This package is licensed for use by Reactiv under a written integration agreement with BetterReviews. See LICENSE.
Installation
yarn add @betterreviews/react-native \
react-native-webview react-native-svg react-native-gesture-handlerPeer 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.
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:
import 'react-native-gesture-handler'; // must be first
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function Root() {
return <GestureHandlerRootView style={{ flex: 1 }}>{/* app */}</GestureHandlerRootView>;
}npm users: React Native 0.74 ships a mismatched
@types/reactpeer range, so a plainnpm installmay fail withERESOLVE. This is an upstream React Native quirk, not specific to this package — install withnpm install --legacy-peer-deps, or use Yarn (which is more lenient). Yarn is recommended for React Native projects.
Quick start
import {
BetterReviewsProvider,
ProductContentBlock,
type Theme,
type Config,
type ProductContentBlockSchema,
} from '@betterreviews/react-native';
function App() {
// Partner host app fetches the three metafield bodies from Shopify
// (storefront API or partner-backend proxy) for the active product.
const theme: Theme | null = useFetchedTheme(productId);
const config: Config | null = useFetchedConfig(productId);
const block: ProductContentBlockSchema | null = useFetchedBlock(productId);
return (
<BetterReviewsProvider
theme={theme}
config={config}
onTelemetryEvent={(event) => {
// Forward to partner's own observability stack.
partnerAnalytics.log(event);
}}
>
<ProductContentBlock block={block} />
</BetterReviewsProvider>
);
}Render gates
<ProductContentBlock> renders nothing (returns null) when any of these are true:
config.product_content_block_enabled === false— merchant explicitly disabled this productconfig.min_sdk_versiondeclared and current SDK below floor — emitsbetterreviews.fetch.failuretelemetryblockisnull/undefined- The block envelope fails top-level schema validation — emits
betterreviews.schema.violationtelemetry
Per-section validation uses tolerant-reader semantics: an individual malformed section is dropped (with telemetry) while the rest render.
Telemetry events
| Event | When |
|---|---|
| betterreviews.fetch.failure | Mount blocked by min_sdk_version floor (error_code: "sdk_below_floor") |
| betterreviews.schema.violation | Envelope or per-section validation failed |
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).
Theming
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).
StarRating (aggregate badge)
<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.
import { StarRating } from '@betterreviews/react-native';
<StarRating average={4.5} total={128} onPress={scrollToReviews} />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).
ReviewWidget (review browsing + voting)
<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.
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.
import {
ReviewWidget,
createBetterReviewsClient,
type Fetcher,
} from '@betterreviews/react-native';
const fetcher: Fetcher = async ({ path, query, method = 'GET', body, signal }) => {
const params = new URLSearchParams();
for (const [k, v] of Object.entries(query ?? {})) if (v !== undefined) params.set(k, String(v));
params.set('token', getWidgetTokenFromSecureConfig()); // host-injected; never hardcode
const res = await fetch(`${API_BASE}${path}?${params}`, {
method, signal,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`widget request failed: HTTP ${res.status}`); // never log the URL — the token is in it
return res.json();
};
const client = createBetterReviewsClient({ fetcher, storeId, productId });
// Theme + telemetry come from <BetterReviewsProvider> context (NOT props).
<BetterReviewsProvider theme={theme} onTelemetryEvent={partnerAnalytics.log}>
<ReviewWidget client={client} onWriteReview={openReviewChat} />
</BetterReviewsProvider>Notes:
ReviewWidgetrenders inline — it owns no scroll container, so drop it into your product-pageScrollViewas one section (e.g. below the product info andProductContentBlock) and it scrolls with the page. Paging is a "Load more" button (no virtualization). The sort drawer and full-screen media viewer areModaloverlays, so they work regardless.onWriteReviewis host-owned (open your chat WebView / nav). The CTA hides if omitted. Never pass server-controlled strings toLinking.openURL.- 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. - The widget requires the
GestureHandlerRootViewroot wrap above.
Security obligations on the host
See SECURITY.md § "What you must do (the host)" 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.
Versioning
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.
WebView surface
For the customer-facing chat flow (/review/chat), use WebViewHost:
import { WebViewHost } from '@betterreviews/react-native';
<WebViewHost
url={`https://api.betterreviews.app/review/chat?store_id=${storeId}&product_id=${productId}&token=${token}`}
onMessage={(event) => console.log('message from chat', event.nativeEvent.data)}
onError={(event) => console.warn('webview error', event.nativeEvent)}
onClose={() => dismissChat()} // fires when the page posts {type:'close'} (e.g. "Back to store")
/>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.
react-native-webview ≥13.0.0 is a peer dep. Add it to your host app:
yarn add react-native-webviewBridge config
config.bridge declares the merchant's preference for native bridge surfaces:
"off"(default): everything renders in-WebView."auto": native if available, fall back to in-WebView."required": native; if not available, render nothing.
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).
Not yet shipped
- Native (non-WebView) bridge surfaces for the chat flow (Card C.14 — post-soft-launch)
- Per-surface theming of the widget's fixed neutrals (star/verified/muted/border/scrim) — a later additive
themeschema bump - HMAC signature verification (forward-compat — added when a bidirectional channel emerges)
License
Proprietary. See LICENSE. For licensing inquiries: [email protected].
