@app-brew/judgeme 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/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@app-brew/judgeme",
3
+ "version": "1.0.0",
4
+ "files": [
5
+ "src/",
6
+ "tsconfig.paths.json"
7
+ ],
8
+ "peerDependencies": {
9
+ "@fastify/deepmerge": "^1.3.0",
10
+ "axios": "^1.8.3",
11
+ "react": "19.1.0",
12
+ "react-native": "0.81.0",
13
+ "react-native-config": "1.5.9",
14
+ "react-native-webview": "13.16.0",
15
+ "@app-brew/brewery": ">=1.0.0"
16
+ }
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { JudgemeReviewsV2 } from './judgeme-reviews'
2
+ import { JudgemeReviewScreen } from './judgeme-screen'
3
+
4
+ export * from './judgeme-review-provider'
5
+ export * from './judgeme-star-ratings'
6
+ export * from './ratings-count'
7
+
8
+ export function registerJudgemeBlocks(r: Map<string, any>) {
9
+ r.set('judgeme-reviews-v2', JudgemeReviewsV2)
10
+ }
11
+
12
+ export function registerJudgemeScreen(r: Map<string, any>) {
13
+ r.set('product-reviews', JudgemeReviewScreen)
14
+ }
@@ -0,0 +1,42 @@
1
+ import axios from 'axios'
2
+ import EnvConfig from 'react-native-config'
3
+ import { ReviewDetails, ReviewProvider } from '@gauntlet/types'
4
+
5
+ export class JudgemeReviewProvider implements ReviewProvider {
6
+ static instance: JudgemeReviewProvider
7
+ static getInstance() {
8
+ if (!JudgemeReviewProvider.instance) {
9
+ JudgemeReviewProvider.instance = new JudgemeReviewProvider()
10
+ }
11
+ return JudgemeReviewProvider.instance
12
+ }
13
+ async submitReview(
14
+ reviewDetails: ReviewDetails,
15
+ customerDetails: {
16
+ customerId: string
17
+ customerAccessToken: string
18
+ email: string
19
+ displayName: string
20
+ }
21
+ ) {
22
+ const appId = EnvConfig.APP_ID
23
+ await axios.post(
24
+ `${EnvConfig['APP_SERVICE_URL']}/integrations/judgeme/reviews`,
25
+ reviewDetails,
26
+ {
27
+ headers: {
28
+ 'X-App-Id': appId,
29
+ 'X-Customer-Access-Token': customerDetails.customerAccessToken,
30
+ 'X-Customer-Id': customerDetails.customerId,
31
+ },
32
+ }
33
+ )
34
+ return
35
+ }
36
+ async getReviewsSummary(productId: string) {
37
+ return {}
38
+ }
39
+ async getReviews(productId: string, page: number) {
40
+ return {}
41
+ }
42
+ }
@@ -0,0 +1,148 @@
1
+ import * as React from 'react'
2
+ import {
3
+ ScreenContext,
4
+ useAppStore,
5
+ useBlock,
6
+ useDeviceDimensions,
7
+ useProductByHandle,
8
+ useUser,
9
+ } from '@gauntlet/state'
10
+ import {
11
+ BaseBlockProps,
12
+ JudgemeReviewsConfig,
13
+ ProductId,
14
+ } from '@gauntlet/types'
15
+ import { Section } from '@gauntlet/components/atoms/section'
16
+ import { Text } from '@gauntlet/components/atoms/text'
17
+ import { View } from 'react-native'
18
+ import { Loader } from '@gauntlet/components/organisms/payment-web-view'
19
+
20
+ import { useBlockSettings } from '@gauntlet/components/style-utils'
21
+ import { WebView } from 'react-native-webview'
22
+ import { useLink } from '@gauntlet/state'
23
+
24
+ export type JudgemeReviewsProps = BaseBlockProps & {
25
+ productHandle: string
26
+ }
27
+ export function JudgemeReviewsV2({
28
+ screenId,
29
+ componentId,
30
+ instanceId,
31
+ productHandle,
32
+ }: JudgemeReviewsProps) {
33
+ const { gotoLink } = useLink()
34
+ const webViewRef = React.useRef<any>(null)
35
+ const { width, height } = useDeviceDimensions()
36
+ const screenContext = React.useContext(ScreenContext)
37
+ const contentHeight = screenContext?.value?.contentHeight ?? 710
38
+
39
+ const block = useBlock<JudgemeReviewsConfig>(
40
+ screenId,
41
+ componentId,
42
+ instanceId
43
+ )
44
+ const settings = useBlockSettings(block)
45
+
46
+ const product = useProductByHandle(productHandle)
47
+ const judgemeConfig = useAppStore.getState().config.data?.integrations.judgeme
48
+ const options = settings?.options
49
+
50
+ const { isUserAuthenticated } = useUser()
51
+
52
+ if (product.status !== 'idle' || !judgemeConfig)
53
+ return (
54
+ <Section
55
+ style={{
56
+ flex: 1,
57
+ alignItems: 'center',
58
+ ...block?.style?.root,
59
+ height: height,
60
+ }}
61
+ >
62
+ <Text>Loading...</Text>
63
+ </Section>
64
+ )
65
+ if (!product.data)
66
+ console.log(
67
+ `Expected product.data to be defined for productHandle ${productHandle} and status ${product.status}`
68
+ )
69
+
70
+ const onMessageReceive = (event: any) => {
71
+ const data = JSON.parse(event.nativeEvent.data)
72
+ if (data.action === 'navigate') {
73
+ if (data.path === 'add-review') {
74
+ if (isUserAuthenticated()) {
75
+ gotoLink({
76
+ kind: 'screen',
77
+ value: `${data.path}/${productHandle}`,
78
+ })
79
+ } else {
80
+ gotoLink({
81
+ kind: 'screen',
82
+ value: options?.signInPath ?? 'signin',
83
+ })
84
+ }
85
+ return
86
+ }
87
+
88
+ gotoLink({
89
+ kind: 'screen',
90
+ value: data.path,
91
+ })
92
+ }
93
+ }
94
+
95
+ if (!judgemeConfig) return null
96
+
97
+ const uri = `https://appbrew.pages.dev/judgeme-reviews?productId=${getJudgemeProductId(
98
+ product.data.id
99
+ )}&domain=${judgemeConfig.storeDomain}&token=${
100
+ judgemeConfig.publicToken
101
+ }&platform=${judgemeConfig.platform ?? 'shopify'}&stylesheet=${
102
+ judgemeConfig.stylesheet
103
+ }`
104
+
105
+ return (
106
+ <WebView
107
+ ref={webViewRef}
108
+ style={{
109
+ height: contentHeight,
110
+ width: width,
111
+ ...block?.style?.root,
112
+ }}
113
+ androidLayerType="hardware"
114
+ startInLoadingState={true}
115
+ androidHardwareAccelerationDisabled={true}
116
+ renderLoading={() => (
117
+ <View
118
+ style={{
119
+ position: 'absolute',
120
+ alignItems: 'center',
121
+ justifyContent: 'center',
122
+ left: 0,
123
+ right: 0,
124
+ top: 0,
125
+ bottom: 0,
126
+ }}
127
+ >
128
+ <Loader colors={['#000', '#000', '#000', '#000']} />
129
+ </View>
130
+ )}
131
+ automaticallyAdjustContentInsets={true}
132
+ scrollEnabled={true}
133
+ javaScriptEnabled={true}
134
+ domStorageEnabled={true}
135
+ nestedScrollEnabled={true}
136
+ source={{
137
+ uri,
138
+ }}
139
+ onMessage={onMessageReceive}
140
+ />
141
+ )
142
+ }
143
+
144
+ function getJudgemeProductId(id: ProductId) {
145
+ if (!id.startsWith('gid')) return null
146
+ const judgemeId = id.replace(/[^0-9]/g, '')
147
+ return judgemeId
148
+ }
@@ -0,0 +1,267 @@
1
+ import { CustomScreenProps } from '@gauntlet/types'
2
+ import { SafeAreaContainer } from '@gauntlet/components/atoms/safe-area-container'
3
+ import * as React from 'react'
4
+ import {
5
+ ScreenContext,
6
+ useAppStore,
7
+ useDeviceDimensions,
8
+ useProductByHandle,
9
+ useUser,
10
+ } from '@gauntlet/state'
11
+ import { ActivityIndicator, View } from 'react-native'
12
+
13
+ import { WebView } from 'react-native-webview'
14
+ import { useLink } from '@gauntlet/state'
15
+ import { EnhancedProduct, IdleData, ProductId } from '@gauntlet/types'
16
+ import { appbarVariants } from '@gauntlet/components/molecules/appbar-v2'
17
+ import deepmerge from '@fastify/deepmerge'
18
+ import { AppbarTemplate } from '@gauntlet/components/molecules/appbar-template'
19
+ import { Text } from '@gauntlet/components/index'
20
+
21
+ const BASE_URL = 'https://appbrew.pages.dev'
22
+
23
+ export function JudgemeReviewScreen({
24
+ id,
25
+ options: screenOptions,
26
+ }: CustomScreenProps) {
27
+ const { productHandle } = screenOptions
28
+ const { gotoLink } = useLink()
29
+ const webViewRef = React.useRef<any>(null)
30
+ const { width } = useDeviceDimensions()
31
+ const screenContext = React.useContext(ScreenContext)
32
+ const contentHeight = screenContext?.value?.contentHeight ?? 710
33
+
34
+ const product = useProductByHandle(productHandle ?? '')
35
+ const judgemeConfig = useAppStore.getState().config.data?.integrations.judgeme
36
+ const reviewsScreen =
37
+ useAppStore.getState().config?.data?.screens?.['product-reviews']
38
+ const judgemeMetafieldExists = Boolean(
39
+ useAppStore.getState().metafield.data.product?.[productHandle]?.judgeme
40
+ ?.badge
41
+ )
42
+ const settings = reviewsScreen?.settings ?? {
43
+ source: {},
44
+ options: {},
45
+ }
46
+
47
+ const cssVariables = React.useMemo(() => {
48
+ if (!judgemeConfig) return {}
49
+ const defaultPrimaryColor =
50
+ useAppStore.getState().config.data?.theme?.base?.colors?.brandprimary ??
51
+ '#000000'
52
+
53
+ const defaultTextOnPrimaryColor =
54
+ useAppStore.getState().config.data?.theme?.base?.colors
55
+ ?.textprimaryinverse ?? '#ffffff'
56
+
57
+ /**
58
+ * primary color: will change the background color of the button and the verified badge
59
+ * text-dark: title and content of the reviews
60
+ * star-color: color of the stars
61
+ * bg-very-light: background color of the reviews
62
+ * text-on-primary: text color on the primary color (button and verified badge)
63
+ * button-border-radius: border radius of the button
64
+ * font-family: font family of the text
65
+ *
66
+ */
67
+ const cssVariablesMap: Record<string, string | number> = {
68
+ primaryColor: judgemeConfig.primaryColor || defaultPrimaryColor,
69
+ starColor: judgemeConfig.starColor || '#ffa41c',
70
+ textDark: judgemeConfig.textDark || '#1a1b18',
71
+ reviewsBackgroundColor: judgemeConfig.bgVeryLight || '#f9f9f9',
72
+ textOnPrimary: judgemeConfig.textOnPrimary || defaultTextOnPrimaryColor,
73
+ buttonBorderRadius: judgemeConfig.buttonBorderRadius ?? 999,
74
+ fontFamily: judgemeConfig.fontFamily || 'Roboto',
75
+ }
76
+
77
+ for (const key in cssVariablesMap) {
78
+ const value = String(cssVariablesMap[key])
79
+ if (value.startsWith('#')) {
80
+ cssVariablesMap[key] = value.replace('#', '')
81
+ } else {
82
+ cssVariablesMap[key] = value
83
+ }
84
+ }
85
+
86
+ return cssVariablesMap
87
+ }, [judgemeConfig])
88
+
89
+ const style = reviewsScreen?.style ?? {}
90
+ const variant = settings?.options?.appbar?.variant ?? 'minimal'
91
+ const appbarVariant = appbarVariants[variant]
92
+
93
+ const { isUserAuthenticated } = useUser()
94
+ const mergedStyle = deepmerge({ all: true })(
95
+ appbarVariant?.style,
96
+ style?.appbar ?? {}
97
+ )
98
+ const finalSettings = settings?.['appbar'] ?? appbarVariant?.settings
99
+
100
+ if (!judgemeMetafieldExists) {
101
+ return (
102
+ <SafeAreaContainer>
103
+ <AppbarTemplate
104
+ options={finalSettings?.options}
105
+ style={mergedStyle}
106
+ source={finalSettings?.source}
107
+ variant={variant}
108
+ />
109
+ <View style={style.error?.root}>
110
+ <Text style={style.error?.text}>
111
+ {settings?.options?.errorMessage ?? 'No reviews to display'}
112
+ </Text>
113
+ </View>
114
+ </SafeAreaContainer>
115
+ )
116
+ }
117
+
118
+ if (product.status !== 'idle' || !judgemeConfig)
119
+ return (
120
+ <SafeAreaContainer>
121
+ <AppbarTemplate
122
+ options={finalSettings?.options}
123
+ style={mergedStyle}
124
+ source={finalSettings?.source}
125
+ variant={variant}
126
+ />
127
+ <ActivityIndicator
128
+ size={'large'}
129
+ color={'#000'}
130
+ style={{
131
+ position: 'absolute',
132
+ alignItems: 'center',
133
+ justifyContent: 'center',
134
+ left: 0,
135
+ right: 0,
136
+ top: 0,
137
+ bottom: 0,
138
+ ...mergedStyle?.loader,
139
+ }}
140
+ />
141
+ </SafeAreaContainer>
142
+ )
143
+ if (!product.data)
144
+ console.log(
145
+ `Expected product.data to be defined for productHandle ${productHandle} and status ${product.status}`
146
+ )
147
+
148
+ const onMessageReceive = (event: any) => {
149
+ const data = JSON.parse(event.nativeEvent.data)
150
+ if (data.action === 'navigate') {
151
+ if (data.path === 'add-review') {
152
+ if (isUserAuthenticated()) {
153
+ gotoLink({
154
+ kind: 'screen',
155
+ value: `${data.path}`,
156
+ params: { productHandle },
157
+ })
158
+ } else {
159
+ gotoLink({
160
+ kind: 'screen',
161
+ value: settings?.options?.signInPath ?? 'signin',
162
+ })
163
+ }
164
+ return
165
+ }
166
+
167
+ gotoLink({
168
+ kind: 'screen',
169
+ value: data.path,
170
+ })
171
+ }
172
+ }
173
+
174
+ if (!judgemeConfig) return null
175
+
176
+ const uri = gerUri(product, judgemeConfig, cssVariables)
177
+
178
+ return (
179
+ <SafeAreaContainer style={style?.screen}>
180
+ <AppbarTemplate
181
+ options={finalSettings?.options}
182
+ style={mergedStyle}
183
+ source={finalSettings?.source}
184
+ variant={variant}
185
+ />
186
+ <WebView
187
+ ref={webViewRef}
188
+ style={{
189
+ height: contentHeight,
190
+ width: width,
191
+ ...mergedStyle?.webview,
192
+ }}
193
+ androidLayerType="hardware"
194
+ startInLoadingState={true}
195
+ androidHardwareAccelerationDisabled={true}
196
+ renderLoading={() => (
197
+ <View
198
+ style={{
199
+ position: 'absolute',
200
+ alignItems: 'center',
201
+ justifyContent: 'center',
202
+ left: 0,
203
+ right: 0,
204
+ top: 0,
205
+ bottom: 0,
206
+ }}
207
+ >
208
+ <ActivityIndicator
209
+ size={'large'}
210
+ color={'#000'}
211
+ style={{
212
+ position: 'absolute',
213
+ alignItems: 'center',
214
+ justifyContent: 'center',
215
+ left: 0,
216
+ right: 0,
217
+ top: 0,
218
+ bottom: 0,
219
+ ...style?.loader,
220
+ }}
221
+ />
222
+ </View>
223
+ )}
224
+ automaticallyAdjustContentInsets={true}
225
+ scrollEnabled={true}
226
+ javaScriptEnabled={true}
227
+ domStorageEnabled={true}
228
+ nestedScrollEnabled={true}
229
+ source={{
230
+ uri,
231
+ }}
232
+ onMessage={onMessageReceive}
233
+ />
234
+ </SafeAreaContainer>
235
+ )
236
+ }
237
+
238
+ function getJudgemeProductId(id: ProductId) {
239
+ if (!id.startsWith('gid')) return null
240
+ const judgemeId = id.replace(/[^0-9]/g, '')
241
+ return judgemeId
242
+ }
243
+
244
+ function gerUri(
245
+ product: IdleData<EnhancedProduct>,
246
+ judgemeConfig: {
247
+ storeDomain: string
248
+ platform: string
249
+ publicToken: string
250
+ stylesheet?: string | undefined
251
+ customHtmlPageUrl?: string | undefined
252
+ },
253
+ cssVariables: Record<string, string | number>
254
+ ) {
255
+ const defaultJudgemeUrl = `${BASE_URL}/judgeme-reviews/index.html`
256
+ const storeDomain = useAppStore.getState().config.data?.store?.shopifyDomain
257
+ const platform = judgemeConfig.platform ?? 'shopify'
258
+
259
+ const uri = judgemeConfig?.customHtmlPageUrl ?? defaultJudgemeUrl
260
+ return `${uri}?productId=${getJudgemeProductId(
261
+ product.data.id
262
+ )}&domain=${storeDomain}&token=${judgemeConfig.publicToken
263
+ }&platform=${platform}&primaryColor=${cssVariables.primaryColor}&textDark=${cssVariables.textDark
264
+ }&starColor=${cssVariables.starColor}&reviewsBackgroundColor=${cssVariables.reviewsBackgroundColor
265
+ }&textOnPrimary=${cssVariables.textOnPrimary}&buttonBorderRadius=${cssVariables.buttonBorderRadius
266
+ }&fontFamily=${cssVariables.fontFamily}`
267
+ }
@@ -0,0 +1,74 @@
1
+ import { BaseIconStyle, BaseStyle, BaseTextStyle } from '@gauntlet/types'
2
+ import * as React from 'react'
3
+ import { ProductComponentsProps } from '@gauntlet/components/molecules/product-components'
4
+ import { View } from 'react-native'
5
+ import StarRating from '@gauntlet/components/atoms/stars'
6
+ import { useAppStore } from '@gauntlet/state'
7
+
8
+ interface JudgemeStarRatingsStyle {
9
+ root: BaseStyle
10
+ text: BaseTextStyle
11
+ star: BaseIconStyle
12
+ container?: BaseStyle
13
+ }
14
+ interface JudgemeStarRatingsOptions {}
15
+
16
+ interface JudgemeStarRatingsProps extends ProductComponentsProps {
17
+ style: JudgemeStarRatingsStyle
18
+ options: JudgemeStarRatingsOptions
19
+ }
20
+
21
+ export function JudgemeStarRatings({
22
+ productData,
23
+ style,
24
+ options,
25
+ }: JudgemeStarRatingsProps) {
26
+ const judgemeBadge = useAppStore(
27
+ (s) => s.metafield.data.product[productData.handle]?.judgeme?.badge
28
+ )
29
+ const badgeValue = judgemeBadge?.value
30
+ if (!judgemeBadge) return null
31
+ if (!badgeValue) return null
32
+ const { averageRating, numberOfReviews } = parseRatingsAndReviews(badgeValue)
33
+ if (!averageRating || !numberOfReviews) return null
34
+
35
+ return (
36
+ <View style={style.root}>
37
+ {!!(averageRating && Number(averageRating)) && (
38
+ <StarRating
39
+ ratings={averageRating}
40
+ views={numberOfReviews}
41
+ style={{
42
+ root: style.root,
43
+ ratingCount: style.text,
44
+ icon: style.star,
45
+ container: style?.container,
46
+ }}
47
+ handle={productData?.handle}
48
+ options={options}
49
+ />
50
+ )}
51
+ </View>
52
+ )
53
+ }
54
+
55
+ // const htmlString = `<div style='display:none' class='jdgm-prev-badge' data-average-rating='4.64' data-number-of-reviews='208' data-number-of-questions='0'> <span class='jdgm-prev-badge__stars' data-score='4.64' tabindex='0' aria-label='4.64 stars' role='button'> <span class='jdgm-star jdgm--on'></span><span class='jdgm-star jdgm--on'></span><span class='jdgm-star jdgm--on'></span><span class='jdgm-star jdgm--on'></span><span class='jdgm-star jdgm--half'></span> </span> <span class='jdgm-prev-badge__text'> 208 reviews </span> </div>`
56
+ export function parseRatingsAndReviews(htmlString: string): {
57
+ averageRating: number | null
58
+ numberOfReviews: number | null
59
+ } {
60
+ const averageRatingRegex = /data-average-rating=['"]([\d.]+)['"]/
61
+ const numberOfReviewsRegex = /data-number-of-reviews=['"](\d+)['"]/
62
+
63
+ const averageRatingMatch = htmlString.match(averageRatingRegex)
64
+ const numberOfReviewsMatch = htmlString.match(numberOfReviewsRegex)
65
+
66
+ const averageRating = averageRatingMatch
67
+ ? parseFloat(averageRatingMatch[1])
68
+ : null
69
+ const numberOfReviews = numberOfReviewsMatch
70
+ ? parseInt(numberOfReviewsMatch[1], 10)
71
+ : null
72
+
73
+ return { averageRating, numberOfReviews }
74
+ }
@@ -0,0 +1,46 @@
1
+ import { BaseIconStyle, BaseStyle, BaseTextStyle } from '@gauntlet/types'
2
+ import * as React from 'react'
3
+ import { ProductComponentsProps } from '@gauntlet/components/molecules/product-components'
4
+ import { View } from 'react-native'
5
+ import { useAppStore } from '@gauntlet/state'
6
+ import { parseRatingsAndReviews } from './judgeme-star-ratings'
7
+ import { Text } from '@gauntlet/components/atoms/text'
8
+
9
+ interface RatingsCountStyle {
10
+ root: BaseStyle
11
+ text: BaseTextStyle
12
+ star: BaseIconStyle
13
+ container?: BaseStyle
14
+ }
15
+ interface RatingsCountOptions {
16
+ template?: string
17
+ }
18
+
19
+ interface RatingsCountProps extends ProductComponentsProps {
20
+ style: RatingsCountStyle
21
+ options: RatingsCountOptions
22
+ }
23
+
24
+ export function RatingsCount({
25
+ productData,
26
+ style,
27
+ options,
28
+ }: RatingsCountProps) {
29
+ const judgemeBadge = useAppStore(
30
+ (s) => s.metafield.data.product[productData.handle]?.judgeme?.badge
31
+ )
32
+ const badgeValue = judgemeBadge?.value
33
+ if (!judgemeBadge) return null
34
+ if (!badgeValue) return null
35
+ const { numberOfReviews } = parseRatingsAndReviews(badgeValue)
36
+ if (!numberOfReviews) return null
37
+ if (Number(numberOfReviews) === 0) return null
38
+ const template = options.template ?? '{{count}}'
39
+ const displayText = template.replace('{{count}}', String(numberOfReviews))
40
+ return (
41
+ <View style={style.root}>
42
+ <Text style={style.text}>{displayText}</Text>
43
+ </View>
44
+ )
45
+ }
46
+
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@gauntlet/integrations/judgeme": [
6
+ "./src/index.ts"
7
+ ]
8
+ }
9
+ }
10
+ }