@app-brew/appbrew 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.
@@ -0,0 +1,107 @@
1
+ import { removeUndefined } from '@gauntlet/analytics'
2
+ import { LocalStorage } from '@gauntlet/local-storage'
3
+ import { useAppStore } from '@gauntlet/state'
4
+ import { AppConfig } from '@gauntlet/types'
5
+ import messaging from '@react-native-firebase/messaging'
6
+ import EnvConfig from 'react-native-config'
7
+
8
+ interface AppUserInterface {
9
+ email?: string
10
+ phoneNumber?: string
11
+ firstName?: string
12
+ lastName?: string
13
+ pushToken: string
14
+ deviceInstanceId: string
15
+ }
16
+
17
+ export class AppUser {
18
+ static USER_KEY = 'app-user'
19
+ static BASE_URL = `${EnvConfig.APP_SERVICE_URL}/users`
20
+ static USER_EXPIRY_IN_MILLISECONDS = 1000 * 60 * 60 * 24 * 15
21
+ static USER_PROPERTIES: (keyof AppUserInterface)[] = [
22
+ 'email',
23
+ 'phoneNumber',
24
+ 'firstName',
25
+ 'lastName',
26
+ 'pushToken',
27
+ 'deviceInstanceId',
28
+ ]
29
+
30
+ static async init(config?: AppConfig) {
31
+ try {
32
+ this.USER_EXPIRY_IN_MILLISECONDS =
33
+ config?.settings?.global?.appUserExpiryInMS ||
34
+ this.USER_EXPIRY_IN_MILLISECONDS
35
+ const userLoggedIn = useAppStore.getState().user.isUserAuthenticated()
36
+ if (!userLoggedIn) {
37
+ await this.updateUser()
38
+ }
39
+ } catch (e) {
40
+ console.log(e)
41
+ }
42
+ }
43
+
44
+ static async updateUser(user?: any) {
45
+ const getInstanceId = useAppStore.getState().analytics.getInstanceId
46
+ const token = await messaging().getToken()
47
+ if (token) {
48
+ const updatedUser = {
49
+ pushToken: token,
50
+ deviceInstanceId: getInstanceId(),
51
+ email: user?.email,
52
+ phoneNumber: user?.phoneNumber,
53
+ firstName: user?.firstName,
54
+ lastName: user?.lastName,
55
+ }
56
+
57
+ removeUndefined(updatedUser)
58
+
59
+ const userUpdated = this.checkIfUserUpdated(updatedUser)
60
+ if (userUpdated) {
61
+ const response = await fetch(`${this.BASE_URL}`, {
62
+ method: 'POST',
63
+ headers: {
64
+ 'Content-Type': 'application/json',
65
+ },
66
+ body: JSON.stringify({ ...updatedUser, appId: EnvConfig.APP_ID }),
67
+ })
68
+ const isUserUpdated = !(await response.json()).error
69
+ if (isUserUpdated) {
70
+ this.updateUserInLocalStorage(updatedUser)
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ private static checkIfUserUpdated(updatedUser: AppUserInterface) {
77
+ const localStorage = LocalStorage.getInstance()
78
+ const currentUser:
79
+ | {
80
+ data: AppUserInterface
81
+ lastSyncedAt: number
82
+ }
83
+ | undefined = localStorage.getJson(this.USER_KEY)
84
+ if (!currentUser || !currentUser.data || !currentUser.lastSyncedAt) {
85
+ return true
86
+ }
87
+
88
+ const expired =
89
+ (currentUser.lastSyncedAt as number) <
90
+ Date.now() - this.USER_EXPIRY_IN_MILLISECONDS
91
+
92
+ const anyChangeInUserProperty = !this.USER_PROPERTIES.every((key) => {
93
+ return updatedUser[key] === currentUser.data[key]
94
+ })
95
+
96
+ const userUpdated = expired || anyChangeInUserProperty
97
+ return userUpdated
98
+ }
99
+
100
+ private static updateUserInLocalStorage(updatedUser: AppUserInterface) {
101
+ const localStorage = LocalStorage.getInstance()
102
+ localStorage.set(this.USER_KEY, {
103
+ data: updatedUser,
104
+ lastSyncedAt: Date.now(),
105
+ })
106
+ }
107
+ }
@@ -0,0 +1,10 @@
1
+ export const trackerConfig = {
2
+ android: {
3
+ firebaseAppId: '1:444706537999:android:11b0d89a996d60c927197a',
4
+ apiKey: 'SYv0XlMnTBONYAo0KSAiAg',
5
+ },
6
+ ios: {
7
+ firebaseAppId: '1:444706537999:ios:6c7563bcd6e3276227197a',
8
+ apiKey: '3ip0WZ0xSl-gUSqwVEYaNQ',
9
+ },
10
+ }
@@ -0,0 +1,10 @@
1
+ export const trackerConfig = {
2
+ android: {
3
+ firebaseAppId: '1:234880715042:android:56fd9849f0883448913d7b',
4
+ apiKey: 'c4EGflbiQIeD2wopPukKsg',
5
+ },
6
+ ios: {
7
+ firebaseAppId: '1:234880715042:ios:e602756a2a4e6c0f913d7b',
8
+ apiKey: '2rzELbueTgORxUF_utePnQ',
9
+ },
10
+ }
@@ -0,0 +1,136 @@
1
+ import { AnalyticsEvent, AnalyticsPayload, AppConfig } from '@gauntlet/types'
2
+ import analytics from '@react-native-firebase/analytics'
3
+ import { Platform } from 'react-native'
4
+ import base64 from 'react-native-base64'
5
+ // To push data to dev BigQuery, update the import to: import { trackerConfig } from './config.dev'
6
+ import { trackerConfig } from './config'
7
+ import { useAppStore } from '@gauntlet/state'
8
+ import { execute } from './utils'
9
+ import { AnalyticsTracker, trimParameters } from '@gauntlet/analytics'
10
+ import { AppUser } from './app-users'
11
+
12
+ export class AppbrewTracker extends AnalyticsTracker {
13
+ ITEMS_PARAMETER_CHARACTERS_LIMIT = 100
14
+ BASE_URL = 'https://www.google-analytics.com/mp/collect'
15
+ appInstanceId: string | null = null
16
+ instanceId: string | null = null
17
+ userId: string | undefined = undefined
18
+ deviceInfo: any = {}
19
+ appInfo: any = {}
20
+ geoInfo: any = {}
21
+ installSourceUtmParams: any = {}
22
+ userProperties: any = {}
23
+ config =
24
+ Platform.OS === 'android' ? trackerConfig['android'] : trackerConfig['ios']
25
+
26
+ async initTracker(config?: AppConfig) {
27
+ try {
28
+ await AppUser.init(config)
29
+ const getUserId = useAppStore.getState().analytics.getUserId
30
+ const getInstanceId = useAppStore.getState().analytics.getInstanceId
31
+ const getDeviceInfo = useAppStore.getState().analytics.getDeviceInfo
32
+ const getGeoInfo = useAppStore.getState().analytics.getGeoInfo
33
+ const getInstallSourceUtmParams =
34
+ useAppStore.getState().analytics.getInstallSourceUtmParams
35
+ const getAppInfo = useAppStore.getState().analytics.getAppInfo
36
+ ;[
37
+ this.userId,
38
+ this.appInstanceId,
39
+ this.instanceId,
40
+ this.deviceInfo,
41
+ this.geoInfo,
42
+ this.installSourceUtmParams,
43
+ this.appInfo,
44
+ ] = await Promise.all([
45
+ execute(getUserId),
46
+ analytics().getAppInstanceId(),
47
+ execute(getInstanceId),
48
+ execute(getDeviceInfo),
49
+ execute(getGeoInfo),
50
+ execute(getInstallSourceUtmParams),
51
+ execute(getAppInfo),
52
+ ])
53
+ const { app_name, app_id, ...appInfo } = this.appInfo
54
+
55
+ const originalUserProperties = {
56
+ ...this.geoInfo,
57
+ ...this.deviceInfo,
58
+ ...appInfo,
59
+ ...this.installSourceUtmParams,
60
+ }
61
+
62
+ Object.keys(originalUserProperties).forEach((key) => {
63
+ this.userProperties[key] = {
64
+ value: originalUserProperties[key],
65
+ }
66
+ })
67
+ } catch (err) {
68
+ console.log('Error occurred while getting instance id')
69
+ }
70
+ }
71
+
72
+ async sendEvent(e: AnalyticsEvent, p: AnalyticsPayload) {
73
+ this.sendMeasurementEvent(e, p)
74
+ }
75
+ async sendScreenView(screenName: string) {
76
+ this.sendMeasurementEvent(AnalyticsEvent.PAGE_VIEW, { screenName })
77
+ }
78
+
79
+ async setUserDetails(user: any) {
80
+ this.userId = user?.email && base64.encode(user?.email)
81
+ await AppUser.updateUser(user)
82
+ }
83
+
84
+ private async sendMeasurementEvent(
85
+ event: AnalyticsEvent,
86
+ payload: AnalyticsPayload
87
+ ) {
88
+ const formattedPayload = trimParameters(
89
+ payload,
90
+ this.ITEMS_PARAMETER_CHARACTERS_LIMIT
91
+ )
92
+
93
+ const getSessionId = useAppStore.getState().analytics.getSessionId
94
+ const getTriggeredFromPush =
95
+ useAppStore.getState().analytics.getTriggeredFromPush
96
+ const sessionId = getSessionId()
97
+ const triggeredByPush = getTriggeredFromPush()
98
+ if (this.appInstanceId && this.instanceId && sessionId) {
99
+ try {
100
+ const { app_name, app_id } = this.appInfo
101
+ const eventSourceUtmParams = await useAppStore
102
+ .getState()
103
+ .analytics.getEventSourceUtmParams()
104
+ await fetch(
105
+ `${this.BASE_URL}?api_secret=${this.config.apiKey}&firebase_app_id=${this.config.firebaseAppId}`,
106
+ {
107
+ method: 'POST',
108
+ body: JSON.stringify({
109
+ app_instance_id: this.appInstanceId,
110
+ user_id: this.userId || this.instanceId,
111
+ non_personalized_ads: true,
112
+ user_properties: this.userProperties,
113
+ events: [
114
+ {
115
+ name: event,
116
+ params: {
117
+ instance_id: this.instanceId,
118
+ session_id: sessionId,
119
+ appbrew_session_id: sessionId,
120
+ triggered_by_push: triggeredByPush,
121
+ app_id,
122
+ app_name,
123
+ ...eventSourceUtmParams,
124
+ ...formattedPayload,
125
+ },
126
+ },
127
+ ],
128
+ }),
129
+ }
130
+ )
131
+ } catch (err) {
132
+ // ignore
133
+ }
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,154 @@
1
+ import {
2
+ AnalyticsEvent,
3
+ AnalyticsEventParams,
4
+ AnalyticsPayload,
5
+ AppConfig,
6
+ } from '@gauntlet/types'
7
+ import analytics from '@react-native-firebase/analytics'
8
+ import { Platform } from 'react-native'
9
+ import base64 from 'react-native-base64'
10
+ // To push data to dev BigQuery, update the import to: import { trackerConfig } from './config.dev'
11
+ import { trackerConfig } from './config'
12
+ import { useAppStore } from '@gauntlet/state'
13
+ import { execute } from './utils'
14
+ import { AnalyticsTrackerV2, trimParameters } from '@gauntlet/analytics'
15
+ import { AppUser } from './app-users'
16
+
17
+ const defaultEventsWhitelist = Object.values(AnalyticsEvent)
18
+ const defaultParamsWhitelist = Object.values(AnalyticsEventParams)
19
+
20
+ export class AppbrewTrackerV2 extends AnalyticsTrackerV2 {
21
+ SESSION_EXPIRY_IN_SECONDS = 30 * 60
22
+ SESSION_KEY = 'appbrewSession'
23
+ ITEMS_PARAMETER_CHARACTERS_LIMIT = 100
24
+ BASE_URL = 'https://www.google-analytics.com/mp/collect'
25
+ appInstanceId: string | null = null
26
+ instanceId: string | null = null
27
+ userId: string | undefined = undefined
28
+ deviceInfo: any = {}
29
+ appInfo: any = {}
30
+ geoInfo: any = {}
31
+ installSourceUtmParams: any = {}
32
+ userProperties: any = {}
33
+ config =
34
+ Platform.OS === 'android' ? trackerConfig['android'] : trackerConfig['ios']
35
+
36
+ async initTracker(config?: AppConfig) {
37
+ try {
38
+ await AppUser.init(config)
39
+
40
+ this.eventsMapper = {}
41
+ this.paramsMapper = {}
42
+ this.eventsWhitelist = defaultEventsWhitelist
43
+ this.paramsWhitelist = defaultParamsWhitelist
44
+
45
+ const getUserId = useAppStore.getState().analytics.getUserId
46
+ const getInstanceId = useAppStore.getState().analytics.getInstanceId
47
+ const getDeviceInfo = useAppStore.getState().analytics.getDeviceInfo
48
+ const getGeoInfo = useAppStore.getState().analytics.getGeoInfo
49
+ const getInstallSourceUtmParams =
50
+ useAppStore.getState().analytics.getInstallSourceUtmParams
51
+ const getAppInfo = useAppStore.getState().analytics.getAppInfo
52
+ ;[
53
+ this.userId,
54
+ this.appInstanceId,
55
+ this.instanceId,
56
+ this.deviceInfo,
57
+ this.geoInfo,
58
+ this.installSourceUtmParams,
59
+ this.appInfo,
60
+ ] = await Promise.all([
61
+ execute(getUserId),
62
+ analytics().getAppInstanceId(),
63
+ execute(getInstanceId),
64
+ execute(getDeviceInfo),
65
+ execute(getGeoInfo),
66
+ execute(getInstallSourceUtmParams),
67
+ execute(getAppInfo),
68
+ ])
69
+ const { app_name, app_id, ...appInfo } = this.appInfo
70
+
71
+ const originalUserProperties = {
72
+ ...this.geoInfo,
73
+ ...this.deviceInfo,
74
+ ...appInfo,
75
+ ...this.installSourceUtmParams,
76
+ }
77
+
78
+ Object.keys(originalUserProperties).forEach((key) => {
79
+ this.userProperties[key] = {
80
+ value: originalUserProperties[key],
81
+ }
82
+ })
83
+ } catch (err) {
84
+ console.log('Error occurred while getting instance id')
85
+ }
86
+ }
87
+
88
+ async sendEvent(e: AnalyticsEvent, p: AnalyticsPayload) {
89
+ this.sendMeasurementEvent(e, p)
90
+ }
91
+ async sendScreenView(screenName: string) {
92
+ this.sendMeasurementEvent(AnalyticsEvent.PAGE_VIEW, {
93
+ [AnalyticsEventParams.SCREEN_NAME]: screenName,
94
+ })
95
+ }
96
+
97
+ async setUserDetails(user: any) {
98
+ this.userId = user?.email && base64.encode(user?.email)
99
+ await AppUser.updateUser(user)
100
+ }
101
+
102
+ private async sendMeasurementEvent(
103
+ event: AnalyticsEvent,
104
+ payload: AnalyticsPayload
105
+ ) {
106
+ const formattedPayload = trimParameters(
107
+ payload,
108
+ this.ITEMS_PARAMETER_CHARACTERS_LIMIT
109
+ )
110
+
111
+ const getSessionId = useAppStore.getState().analytics.getSessionId
112
+ const getTriggeredFromPush =
113
+ useAppStore.getState().analytics.getTriggeredFromPush
114
+ const sessionId = getSessionId()
115
+ const triggeredByPush = getTriggeredFromPush()
116
+ if (this.appInstanceId && this.instanceId && sessionId) {
117
+ try {
118
+ const { app_name, app_id } = this.appInfo
119
+ const eventSourceUtmParams = await useAppStore
120
+ .getState()
121
+ .analytics.getEventSourceUtmParams()
122
+ await fetch(
123
+ `${this.BASE_URL}?api_secret=${this.config.apiKey}&firebase_app_id=${this.config.firebaseAppId}`,
124
+ {
125
+ method: 'POST',
126
+ body: JSON.stringify({
127
+ app_instance_id: this.appInstanceId,
128
+ user_id: this.userId || this.instanceId,
129
+ non_personalized_ads: true,
130
+ user_properties: this.userProperties,
131
+ events: [
132
+ {
133
+ name: event,
134
+ params: {
135
+ instance_id: this.instanceId,
136
+ session_id: sessionId,
137
+ appbrew_session_id: sessionId,
138
+ triggered_by_push: triggeredByPush,
139
+ app_id,
140
+ app_name,
141
+ ...eventSourceUtmParams,
142
+ ...formattedPayload,
143
+ },
144
+ },
145
+ ],
146
+ }),
147
+ }
148
+ )
149
+ } catch (err) {
150
+ // ignore
151
+ }
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,9 @@
1
+ import * as Sentry from '@sentry/react-native'
2
+ export async function execute(func: any) {
3
+ try {
4
+ return await func()
5
+ } catch (e) {
6
+ console.log(e)
7
+ Sentry.captureException(e)
8
+ }
9
+ }
@@ -0,0 +1,203 @@
1
+ import {
2
+ AnalyticsEvent,
3
+ AnalyticsEventParams,
4
+ AnalyticsPayload,
5
+ AppConfig,
6
+ } from '@gauntlet/types'
7
+ import { AnalyticsTrackerV2 } from '@gauntlet/analytics'
8
+ import { useAppStore } from '@gauntlet/state'
9
+
10
+ export interface WebhookTrackerConfig {
11
+ apiUrl?: string
12
+ eventUrls?: Record<string, string>
13
+ eventsWhitelist?: AnalyticsEvent[]
14
+ paramsWhitelist?: AnalyticsEventParams[]
15
+ headers?: Record<string, string>
16
+ }
17
+
18
+ export class WebhookTracker extends AnalyticsTrackerV2 {
19
+ private trackerId: string
20
+ private defaultApiUrl = ''
21
+ private eventUrls: Record<string, string> = {}
22
+ private customHeaders: Record<string, string> = {}
23
+ private userDetails: {
24
+ email?: string
25
+ phone?: string
26
+ firstName?: string
27
+ lastName?: string
28
+ customerId?: string
29
+ } = {}
30
+ private instanceId: string | null = null
31
+ private sessionId: string | null = null
32
+ private deviceInfo: any = {}
33
+ private appInfo: any = {}
34
+
35
+ constructor(trackerId: string) {
36
+ super()
37
+ this.trackerId = trackerId
38
+ }
39
+
40
+ override async initTracker(config?: AppConfig) {
41
+ // Look for config by tracker ID: config.integrations.webhookTrackers[trackerId]
42
+ const trackerConfig =
43
+ config?.integrations?.[this.trackerId] ??
44
+ (config?.integrations?.webhookTrackers?.[this.trackerId] as
45
+ | WebhookTrackerConfig
46
+ | undefined)
47
+
48
+ if (!trackerConfig) {
49
+ console.warn(`WebhookTracker [${this.trackerId}]: No config found`)
50
+ return
51
+ }
52
+
53
+ if (!trackerConfig.apiUrl && !trackerConfig.eventUrls) {
54
+ console.warn(
55
+ `WebhookTracker [${this.trackerId}]: No API URL or event URLs configured`
56
+ )
57
+ return
58
+ }
59
+
60
+ this.defaultApiUrl = trackerConfig.apiUrl || ''
61
+ this.eventUrls = trackerConfig.eventUrls || {}
62
+ this.customHeaders = trackerConfig.headers || {}
63
+
64
+ // Set whitelists from config
65
+ this.eventsWhitelist = trackerConfig.eventsWhitelist || []
66
+ this.paramsWhitelist = trackerConfig.paramsWhitelist || []
67
+
68
+ // Get instance and device info
69
+ const getInstanceId = useAppStore.getState().analytics.getInstanceId
70
+ const getDeviceInfo = useAppStore.getState().analytics.getDeviceInfo
71
+ const getAppInfo = useAppStore.getState().analytics.getAppInfo
72
+
73
+ try {
74
+ const [instanceId, deviceInfo, appInfo] = await Promise.all([
75
+ getInstanceId(),
76
+ getDeviceInfo(),
77
+ getAppInfo(),
78
+ ])
79
+
80
+ this.instanceId = instanceId
81
+ this.deviceInfo = deviceInfo
82
+ this.appInfo = appInfo
83
+ } catch (err) {
84
+ console.error(
85
+ `WebhookTracker [${this.trackerId}]: Error initializing tracker`,
86
+ err
87
+ )
88
+ }
89
+ }
90
+
91
+ override async setUserDetails(user: any) {
92
+ if (user) {
93
+ this.userDetails = {
94
+ email: user.email,
95
+ phone: user.phone || user.phoneNumber,
96
+ firstName: user.firstName,
97
+ lastName: user.lastName,
98
+ customerId: user.id?.replace('gid://shopify/Customer/', ''),
99
+ }
100
+ }
101
+ }
102
+
103
+ override async sendEvent(event: AnalyticsEvent, payload: AnalyticsPayload) {
104
+ const url = this.getUrlForEvent(event)
105
+
106
+ if (!url || !event) return
107
+
108
+ // Check whitelist - if configured, only send whitelisted events
109
+ if (
110
+ this.eventsWhitelist.length > 0 &&
111
+ !this.eventsWhitelist.includes(event)
112
+ ) {
113
+ return
114
+ }
115
+
116
+ try {
117
+ const getSessionId = useAppStore.getState().analytics.getSessionId
118
+ this.sessionId = getSessionId()
119
+
120
+ const eventPayload = {
121
+ trackerId: this.trackerId,
122
+ event,
123
+ timestamp: new Date().toISOString(),
124
+ user: this.userDetails,
125
+ session: {
126
+ sessionId: this.sessionId,
127
+ instanceId: this.instanceId,
128
+ },
129
+ device: this.deviceInfo,
130
+ app: this.appInfo,
131
+ payload,
132
+ }
133
+
134
+ await fetch(url, {
135
+ method: 'POST',
136
+ headers: {
137
+ 'Content-Type': 'application/json',
138
+ ...this.customHeaders,
139
+ },
140
+ body: JSON.stringify(eventPayload),
141
+ })
142
+ } catch (err) {
143
+ console.error(
144
+ `WebhookTracker [${this.trackerId}]: Error sending event`,
145
+ err
146
+ )
147
+ }
148
+ }
149
+
150
+ override async sendScreenView(screenName: string) {
151
+ const url = this.getUrlForEvent(AnalyticsEvent.SCREEN_VIEW)
152
+
153
+ if (!url) return
154
+
155
+ // Check whitelist - if configured, only send if screen_view is whitelisted
156
+ if (
157
+ this.eventsWhitelist.length > 0 &&
158
+ !this.eventsWhitelist.includes(AnalyticsEvent.SCREEN_VIEW)
159
+ ) {
160
+ return
161
+ }
162
+
163
+ try {
164
+ const getSessionId = useAppStore.getState().analytics.getSessionId
165
+ this.sessionId = getSessionId()
166
+
167
+ const screenViewPayload = {
168
+ trackerId: this.trackerId,
169
+ event: AnalyticsEvent.SCREEN_VIEW,
170
+ timestamp: new Date().toISOString(),
171
+ user: this.userDetails,
172
+ session: {
173
+ sessionId: this.sessionId,
174
+ instanceId: this.instanceId,
175
+ },
176
+ device: this.deviceInfo,
177
+ app: this.appInfo,
178
+ payload: {
179
+ screen_name: screenName,
180
+ },
181
+ }
182
+
183
+ await fetch(url, {
184
+ method: 'POST',
185
+ headers: {
186
+ 'Content-Type': 'application/json',
187
+ ...this.customHeaders,
188
+ },
189
+ body: JSON.stringify(screenViewPayload),
190
+ })
191
+ } catch (err) {
192
+ console.error(
193
+ `WebhookTracker [${this.trackerId}]: Error sending screen view`,
194
+ err
195
+ )
196
+ }
197
+ }
198
+
199
+ private getUrlForEvent(event: string): string | null {
200
+ // Check for event-specific URL first, then fall back to default
201
+ return this.eventUrls[event] || this.defaultApiUrl || null
202
+ }
203
+ }
@@ -0,0 +1 @@
1
+ export * from './provider'
@@ -0,0 +1,211 @@
1
+ import {
2
+ AppConfig,
3
+ AsyncData,
4
+ ConversionRates,
5
+ Country,
6
+ CountryCode,
7
+ CountryCodeToCountryMap,
8
+ CurrencyCode,
9
+ CurrencyCodeToSymbolMap,
10
+ IAvailableCountryResponse,
11
+ ICurrencyProvider,
12
+ } from '@gauntlet/types'
13
+ import EnvConfig from 'react-native-config'
14
+
15
+ type Settings = {
16
+ defaultCountry: string
17
+ supportedCountries: Array<string>
18
+ }
19
+
20
+ const BASE_URL = `${EnvConfig['APP_SERVICE_URL']}/integrations/appbrew-currency/conversion-rates`
21
+ export class AppbrewCurrencyProvider implements ICurrencyProvider {
22
+ static instance: AppbrewCurrencyProvider
23
+ static country: Country
24
+ private response: AsyncData<any>
25
+ private fetchPromise: Promise<any> | undefined
26
+
27
+ static getInstance(appConfig: AppConfig): AppbrewCurrencyProvider {
28
+ if (!AppbrewCurrencyProvider.instance) {
29
+ AppbrewCurrencyProvider.instance = new AppbrewCurrencyProvider(appConfig)
30
+ }
31
+ return AppbrewCurrencyProvider.instance
32
+ }
33
+
34
+ constructor(private readonly appConfig: AppConfig) {
35
+ this.response = { status: 'init' }
36
+ }
37
+
38
+ async getAvailableCountries(
39
+ appConfig: AppConfig
40
+ ): Promise<AsyncData<IAvailableCountryResponse>> {
41
+ const moduleSettings = getCurrencySettings(appConfig)
42
+ if (!moduleSettings) {
43
+ return {
44
+ status: 'error',
45
+ error: {
46
+ message: `Appbrew Currency module not configured`,
47
+ rootCause:
48
+ 'Appbrew Currency module not configured at getAvailableCountries',
49
+ code: `APPBREW_CURRENCY_MODULE_NOT_CONFIGURED`,
50
+ },
51
+ }
52
+ }
53
+ const defaultCountryCode = moduleSettings.defaultCountryCode
54
+ const supportedCountries = moduleSettings.supportedCountries
55
+ const currencyCodeMapping = moduleSettings?.currencyCodeMapping // This is an optional mapping for particular country - country code for showing currency, if not provided, it will use the default currency code from CountryCodeToCountryMap
56
+
57
+ const availableCountries = supportedCountries?.map((c: string) => {
58
+ return {
59
+ isoCode: c as CountryCode,
60
+ name: CountryCodeToCountryMap[c].countryName,
61
+ currency: {
62
+ isoCode:
63
+ CountryCodeToCountryMap[currencyCodeMapping?.[c]]?.currencyCode ??
64
+ (CountryCodeToCountryMap[c].currencyCode as CurrencyCode),
65
+ symbol:
66
+ CurrencyCodeToSymbolMap[
67
+ CountryCodeToCountryMap[currencyCodeMapping?.[c]]?.currencyCode
68
+ ] ??
69
+ CurrencyCodeToSymbolMap[
70
+ CountryCodeToCountryMap?.[c].currencyCode
71
+ ] ??
72
+ CountryCodeToCountryMap[c].currencyCode,
73
+ name:
74
+ CountryCodeToCountryMap[currencyCodeMapping?.[c]]?.currencyCode ??
75
+ CountryCodeToCountryMap[c].currencyCode,
76
+ },
77
+ }
78
+ })
79
+ const defaultCountry = {
80
+ isoCode: defaultCountryCode as CountryCode,
81
+ name: CountryCodeToCountryMap[defaultCountryCode].countryName,
82
+ currency: {
83
+ isoCode: CountryCodeToCountryMap[defaultCountryCode]
84
+ .currencyCode as CurrencyCode,
85
+ symbol:
86
+ CurrencyCodeToSymbolMap[
87
+ CountryCodeToCountryMap[defaultCountryCode].currencyCode
88
+ ] ?? CountryCodeToCountryMap[defaultCountryCode].currencyCode,
89
+ name: CountryCodeToCountryMap[defaultCountryCode].currencyCode,
90
+ },
91
+ }
92
+ AppbrewCurrencyProvider.country = defaultCountry
93
+
94
+ const res: AsyncData<IAvailableCountryResponse> = {
95
+ status: 'idle',
96
+ data: {
97
+ availableCountries: availableCountries,
98
+ },
99
+ }
100
+ return res
101
+ }
102
+ async updateCountryCode(appConfig: AppConfig, countryCode: CountryCode) {
103
+ const country = {
104
+ isoCode: countryCode,
105
+ name: CountryCodeToCountryMap[countryCode].countryName,
106
+ currency: {
107
+ isoCode: CountryCodeToCountryMap[countryCode]
108
+ .currencyCode as CurrencyCode,
109
+ symbol:
110
+ CurrencyCodeToSymbolMap[
111
+ CountryCodeToCountryMap[countryCode].currencyCode
112
+ ] ?? CountryCodeToCountryMap[countryCode].currencyCode,
113
+ name: CountryCodeToCountryMap[countryCode].currencyCode,
114
+ },
115
+ }
116
+ AppbrewCurrencyProvider.country = country
117
+ return country.isoCode
118
+ }
119
+ async getConversionrates(
120
+ appConfig: AppConfig
121
+ ): Promise<AsyncData<ConversionRates>> {
122
+ if (this.response.status === 'idle') return this.response
123
+ const moduleSettings = getCurrencySettings(appConfig)
124
+ if (!moduleSettings) {
125
+ return {
126
+ status: 'error',
127
+ error: {
128
+ message: `Appbrew Currency module not configured`,
129
+ rootCause:
130
+ 'Appbrew Currency module not configured at getConversionrates',
131
+ code: `APPBREW_CURRENCY_MODULE_NOT_CONFIGURED`,
132
+ },
133
+ }
134
+ }
135
+ const defaultCountryCode = moduleSettings.defaultCountryCode
136
+ const supportedCountries = moduleSettings.supportedCountries
137
+ const bCurrency = CountryCodeToCountryMap[defaultCountryCode].currencyCode
138
+ const cCodes = supportedCountries.map(
139
+ (countryCode) => CountryCodeToCountryMap[countryCode].currencyCode
140
+ )
141
+ if (this.response.status === 'loading') {
142
+ if (this.fetchPromise) {
143
+ const result = await this.fetchPromise
144
+ return result
145
+ }
146
+ }
147
+ this.response = {
148
+ status: 'loading',
149
+ }
150
+ this.fetchPromise = fetchConversionRate(BASE_URL, bCurrency, cCodes)
151
+ const result = await this.fetchPromise
152
+ this.response = result
153
+ this.fetchPromise = undefined
154
+ return this.response
155
+ }
156
+ }
157
+
158
+ function getCurrencySettings(
159
+ appConfig: AppConfig
160
+ ): AppConfig['integrations']['appbrew-currency'] {
161
+ if (!appConfig) return
162
+ return (
163
+ appConfig.settings?.['integrations']?.['appbrew-currency'] ??
164
+ appConfig.settings?.['appbrew-currency']
165
+ )
166
+ }
167
+ async function fetchConversionRate(
168
+ baseUrl: string,
169
+ baseCurrencyCode: string,
170
+ currencyCodes: Array<string>
171
+ ) {
172
+ let url = `${BASE_URL}?baseCurrencyCode=${baseCurrencyCode}`
173
+
174
+ if (currencyCodes) {
175
+ url = url.concat(`&currencyCodes=${currencyCodes.join(',')}`)
176
+ }
177
+ try {
178
+ const res = await fetch(url, {
179
+ method: 'GET',
180
+ headers: {
181
+ 'Content-Type': 'application/json',
182
+ 'x-integration-app-name': 'AppBrew',
183
+ 'x-app-id': EnvConfig.APP_ID,
184
+ },
185
+ })
186
+ if (res.status >= 400) {
187
+ return {
188
+ status: 'error',
189
+ error: {
190
+ message: `Server error, Please try after some time`,
191
+ rootCause: 'API error',
192
+ code: `API_ERROR_${res.status}`,
193
+ },
194
+ }
195
+ }
196
+ const result = await res.json()
197
+ return {
198
+ status: 'idle',
199
+ data: result,
200
+ }
201
+ } catch (e: any) {
202
+ return {
203
+ status: 'error',
204
+ error: {
205
+ message: 'fetch faile with exception',
206
+ rootCause: e.message,
207
+ code: `NETWORK_ERROR`,
208
+ },
209
+ }
210
+ }
211
+ }
@@ -0,0 +1,229 @@
1
+ import { Column, Row } from '@gauntlet/components/atoms/flex'
2
+ import {
3
+ useBlockSettings,
4
+ useBlockStyle,
5
+ } from '@gauntlet/components/style-utils'
6
+ import { useBlock, useEdd } from '@gauntlet/state'
7
+ import { BaseBlockProps } from '@gauntlet/types'
8
+ import { Text } from '@gauntlet/components/atoms/text'
9
+ import React from 'react'
10
+ import { Input } from '@gauntlet/components/atoms/input'
11
+ import { Button } from '@gauntlet/ui-builder'
12
+ import { Keyboard } from 'react-native'
13
+ import { add, format } from 'date-fns'
14
+
15
+ export type AppbrewEstimatedDeliveryDateProps = BaseBlockProps
16
+
17
+ export function AppbrewEstimatedDeliveryDate({
18
+ componentId,
19
+ instanceId,
20
+ screenId,
21
+ }: AppbrewEstimatedDeliveryDateProps) {
22
+ const block = useBlock<any>(screenId, componentId, instanceId)
23
+ const style = useBlockStyle(block)
24
+ const { source, options } = useBlockSettings(block)
25
+ const pincodeRef = React.useRef<string>('')
26
+
27
+ const [pincode, setPincode] = React.useState('')
28
+
29
+ const [loading, setLoading] = React.useState(false)
30
+
31
+ const defaultEdd = {
32
+ codAvailable: null,
33
+ serviceable: null,
34
+ pincodeValidity: true,
35
+ minDeliveryDate: '',
36
+ maxDeliveryDate: '',
37
+ express: false,
38
+ pincodeRequired: false,
39
+ }
40
+
41
+ const [
42
+ {
43
+ codAvailable,
44
+ pincodeValidity,
45
+ serviceable,
46
+ minDeliveryDate,
47
+ maxDeliveryDate,
48
+ pincodeRequired,
49
+ },
50
+ setEddRes,
51
+ ] = React.useState<{
52
+ codAvailable: boolean | null
53
+ serviceable: boolean | null
54
+ pincodeValidity: boolean
55
+ minDeliveryDate: string
56
+ maxDeliveryDate: string
57
+ express: boolean
58
+ pincodeRequired: boolean
59
+ }>({
60
+ ...defaultEdd,
61
+ })
62
+ const { fetchEstimatedDeliveryDate } = useEdd()
63
+
64
+ const handleSubmit = React.useCallback(() => {
65
+ Keyboard.dismiss()
66
+
67
+ if (!pincodeRef?.current) {
68
+ setEddRes((res) => ({
69
+ ...res,
70
+ pincodeRequired: true,
71
+ }))
72
+ return
73
+ }
74
+
75
+ if (
76
+ (pincodeRef?.current?.length !== options?.pincodeLength ?? 6) ||
77
+ /^-?\d+$/.test(pincodeRef?.current) === false
78
+ ) {
79
+ setEddRes((res) => ({
80
+ ...res,
81
+ pincodeValidity: false,
82
+ serviceable: true,
83
+ codAvailable: true,
84
+ }))
85
+ return
86
+ }
87
+
88
+ setLoading(true)
89
+
90
+ return fetchEstimatedDeliveryDate(pincodeRef?.current)
91
+ .then((res) => {
92
+ if (res.isServiceable === false) {
93
+ setEddRes((res) => ({
94
+ ...res,
95
+ pincodeValidity: true,
96
+ serviceable: false,
97
+ }))
98
+ return
99
+ }
100
+
101
+ const cutoffDate = new Date(res.defaultCutoffTime * 1000)
102
+ const cutoffHours = cutoffDate.getUTCHours()
103
+ const cutoffMinutes = cutoffDate.getUTCMinutes()
104
+
105
+ // Get the current time
106
+ const currentDate = new Date()
107
+ const currentHours = currentDate.getUTCHours()
108
+ const currentMinutes = currentDate.getUTCMinutes()
109
+
110
+ // Compare the time components
111
+ let hasCutoffTimePassed = false
112
+ if (currentHours > cutoffHours) {
113
+ hasCutoffTimePassed = true
114
+ } else if (currentHours === cutoffHours) {
115
+ if (currentMinutes > cutoffMinutes) {
116
+ hasCutoffTimePassed = true
117
+ }
118
+ }
119
+
120
+ const minDate = format(
121
+ add(new Date(), {
122
+ days: Number(res.maxSLAInDays) + (hasCutoffTimePassed ? 1 : 0),
123
+ }),
124
+ 'EEEE, LLL d'
125
+ )
126
+
127
+ const maxDate = format(
128
+ add(new Date(), {
129
+ days: Number(res.maxSLAInDays) + (hasCutoffTimePassed ? 1 : 0),
130
+ }),
131
+ 'EEEE, LLL d'
132
+ )
133
+
134
+ const serviceable = res.isServiceable.toLowerCase() === 'true' ?? true
135
+ const codAvailable = res.isCODAvailable.toLowerCase() === 'true'
136
+
137
+ const pincodeValidity = true
138
+ const eddRes = {
139
+ minDeliveryDate: minDate,
140
+ maxDeliveryDate: maxDate,
141
+ codAvailable,
142
+ serviceable,
143
+ pincodeValidity,
144
+ express: Boolean(res.hyperlocal),
145
+ }
146
+ setEddRes(eddRes)
147
+ })
148
+ .catch((err) => {
149
+ console.error({ err })
150
+ })
151
+ .finally(() => {
152
+ setLoading(false)
153
+ })
154
+ }, [
155
+ fetchEstimatedDeliveryDate,
156
+ options?.additionalDays,
157
+ options?.pincodeLength,
158
+ ])
159
+
160
+ return (
161
+ <Column style={style?.container}>
162
+ <Text style={style?.title}>{source?.title}</Text>
163
+ <Row style={{ marginTop: 16, ...style?.inputContainer }}>
164
+ <Input
165
+ placeholder={source?.inputPlaceholder}
166
+ placeholderTextColor={style?.inputPlaceholderColor}
167
+ onChangeText={(text) => {
168
+ pincodeRef.current = text
169
+ setPincode(text)
170
+ setEddRes(defaultEdd)
171
+ }}
172
+ style={{
173
+ borderColor: pincodeValidity ? 'black' : 'red',
174
+ borderWidth: 1,
175
+ ...style?.input,
176
+ }}
177
+ keyboardType={'numeric'}
178
+ onSubmitEditing={handleSubmit}
179
+ />
180
+
181
+ <Button
182
+ onPress={handleSubmit}
183
+ text={source?.buttonText}
184
+ style={{
185
+ root: style.button.root,
186
+ text: style.button.text,
187
+ ...style.button,
188
+ }}
189
+ // disabled={pincode?.length <= 0}
190
+ loading={loading}
191
+ />
192
+ </Row>
193
+ {Boolean(
194
+ serviceable !== false &&
195
+ pincodeValidity &&
196
+ (minDeliveryDate || maxDeliveryDate)
197
+ ) && (
198
+ <Text style={style?.deliveryDateText}>
199
+ {Boolean(source?.template) && (
200
+ <Text style={style?.template}>{source?.template}</Text>
201
+ )}
202
+ {minDeliveryDate === maxDeliveryDate
203
+ ? `Expected delivery by ${minDeliveryDate}`
204
+ : `Expected delivery between ${minDeliveryDate} and ${maxDeliveryDate}`}
205
+ </Text>
206
+ )}
207
+
208
+ {Boolean(codAvailable && serviceable !== false && pincodeValidity) && (
209
+ <Text style={style?.codAvailableText}>{source?.codAvailableText}</Text>
210
+ )}
211
+
212
+ {Boolean(serviceable === false) && (
213
+ <Text style={style?.notServiceableText}>
214
+ {source?.notServiceableText}
215
+ </Text>
216
+ )}
217
+
218
+ {Boolean(!pincodeValidity) && (
219
+ <Text style={style?.invalidPincodeText}>
220
+ {source?.invalidPincodeText}
221
+ </Text>
222
+ )}
223
+
224
+ {Boolean(pincodeRequired) && (
225
+ <Text style={style?.pincodeRequired}>{source?.pincodeRequired}</Text>
226
+ )}
227
+ </Column>
228
+ )
229
+ }
@@ -0,0 +1,2 @@
1
+ export * from './provider'
2
+ export * from './blocks/estimated-delivery-date'
@@ -0,0 +1,45 @@
1
+ import { EstimatedDeliveryDateProvider } from '@gauntlet/types'
2
+ import EnvConfig from 'react-native-config'
3
+
4
+ export class AppbrewEstimatedDeliveryDateProvider
5
+ implements EstimatedDeliveryDateProvider
6
+ {
7
+ BASE_URL
8
+ private responseCache = new Map<string, any>()
9
+ static instance: AppbrewEstimatedDeliveryDateProvider
10
+
11
+ static getInstance() {
12
+ if (!AppbrewEstimatedDeliveryDateProvider.instance) {
13
+ AppbrewEstimatedDeliveryDateProvider.instance =
14
+ new AppbrewEstimatedDeliveryDateProvider()
15
+ }
16
+ return AppbrewEstimatedDeliveryDateProvider.instance
17
+ }
18
+ constructor() {
19
+ this.BASE_URL = `${EnvConfig['APP_SERVICE_URL']}/integrations/appbrew-edd/estimated-delivery-details`
20
+ }
21
+
22
+ estimateDeliveryDate = async (dropPincode: string, appId: string) => {
23
+ if (this.responseCache.has(dropPincode)) {
24
+ return this.responseCache.get(dropPincode)
25
+ }
26
+
27
+ const res = await fetch(`${this.BASE_URL}`, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ 'x-app-id': appId,
32
+ },
33
+ body: JSON.stringify({
34
+ zipCode: dropPincode,
35
+ }),
36
+ })
37
+
38
+ const jsonRes = await res.json()
39
+
40
+ if (res.ok) {
41
+ this.responseCache.set(dropPincode, jsonRes)
42
+ return jsonRes
43
+ } else throw new Error('Could not fetch estimated delivery date')
44
+ }
45
+ }
@@ -0,0 +1 @@
1
+ export * from "./provider"
@@ -0,0 +1,88 @@
1
+ import { LocalStorage } from '@gauntlet/local-storage'
2
+ import { WishlistItem, WishlistProvider } from '@gauntlet/types'
3
+
4
+ export class LocalWishlistProvider implements WishlistProvider {
5
+ static instance: LocalWishlistProvider
6
+
7
+ static getInstance() {
8
+ if (LocalWishlistProvider.instance) {
9
+ return LocalWishlistProvider.instance
10
+ }
11
+ LocalWishlistProvider.instance = new LocalWishlistProvider()
12
+
13
+ return LocalWishlistProvider.instance
14
+ }
15
+
16
+ WISHLIST_NATIVE_APPBREW_KEY = {
17
+ wishlistItems: 'wishlist-items',
18
+ }
19
+
20
+ init() {
21
+ if (!this.isInitialised()) {
22
+ LocalStorage.getInstance().set(
23
+ this.WISHLIST_NATIVE_APPBREW_KEY.wishlistItems,
24
+ JSON.stringify([])
25
+ )
26
+ }
27
+ return Promise.resolve()
28
+ }
29
+
30
+ getWishlist() {
31
+ const wishlistItems = getStoredValue(
32
+ this.WISHLIST_NATIVE_APPBREW_KEY.wishlistItems
33
+ )
34
+ return Promise.resolve(wishlistItems)
35
+ }
36
+
37
+ addToWishlist(products: WishlistItem[]) {
38
+ const wishlistItemsJson = getStoredValue(
39
+ this.WISHLIST_NATIVE_APPBREW_KEY.wishlistItems
40
+ )
41
+ if (wishlistItemsJson) {
42
+ const wishlistItemsList = Array.from(
43
+ new Set([...wishlistItemsJson.concat(products)])
44
+ )
45
+ LocalStorage.getInstance().set(
46
+ this.WISHLIST_NATIVE_APPBREW_KEY.wishlistItems,
47
+ wishlistItemsList as any
48
+ )
49
+ }
50
+ return Promise.resolve()
51
+ }
52
+
53
+ removeFromWishlist(itemsToBeRemoved: WishlistItem[]) {
54
+ const itemsInWishlist = getStoredValue(
55
+ this.WISHLIST_NATIVE_APPBREW_KEY.wishlistItems
56
+ )
57
+ const updatedList: Array<WishlistItem> = []
58
+ if (Array.isArray(itemsInWishlist)) {
59
+ for (const item of itemsInWishlist) {
60
+ const itemToBeRemoved = itemsToBeRemoved.find(
61
+ (product) => product.productId === item.productId
62
+ )
63
+ if (!itemToBeRemoved) {
64
+ updatedList.push(item)
65
+ }
66
+ }
67
+
68
+ LocalStorage.getInstance().set(
69
+ this.WISHLIST_NATIVE_APPBREW_KEY.wishlistItems,
70
+ updatedList as any
71
+ )
72
+ }
73
+ return Promise.resolve()
74
+ }
75
+
76
+ isInitialised() {
77
+ const wishlist = getStoredValue(
78
+ this.WISHLIST_NATIVE_APPBREW_KEY.wishlistItems
79
+ )
80
+ return Array.isArray(wishlist)
81
+ }
82
+ }
83
+
84
+ function getStoredValue(key: string) {
85
+ return LocalStorage.getInstance().getJson(key) as
86
+ | Array<WishlistItem>
87
+ | undefined
88
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@app-brew/appbrew",
3
+ "version": "1.0.0",
4
+ "files": [
5
+ "analytics/",
6
+ "currency/",
7
+ "estimated-delivery-date/",
8
+ "native-wishlist/",
9
+ "src/",
10
+ "tsconfig.paths.json"
11
+ ],
12
+ "peerDependencies": {
13
+ "@react-native-firebase/analytics": "23.3.1",
14
+ "@react-native-firebase/messaging": "23.3.1",
15
+ "@sentry/react-native": "7.8.0",
16
+ "date-fns": "2.30.0",
17
+ "react": "19.1.0",
18
+ "react-native": "0.81.0",
19
+ "react-native-base64": "^0.2.1",
20
+ "react-native-config": "1.5.9",
21
+ "@app-brew/brewery": ">=1.0.0"
22
+ }
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './lib/integrations-appbrew'
2
+ export * from '../native-wishlist/src'
3
+ export * from '../analytics/tracker'
4
+ export * from '../analytics/trackerV2'
5
+ export * from '../analytics/webhook-tracker'
6
+ export * from '../currency'
7
+ export * from '../estimated-delivery-date'
@@ -0,0 +1,7 @@
1
+ import { integrationsAppbrew } from './integrations-appbrew'
2
+
3
+ describe('integrationsAppbrew', () => {
4
+ it('should work', () => {
5
+ expect(integrationsAppbrew()).toEqual('integrations-appbrew')
6
+ })
7
+ })
@@ -0,0 +1,3 @@
1
+ export function integrationsAppbrew(): string {
2
+ return 'integrations-appbrew'
3
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@gauntlet/integrations/appbrew": [
6
+ "./src/index.ts"
7
+ ]
8
+ }
9
+ }
10
+ }