@planningcenter/chat-react-native 1.4.2-rc.1 → 1.4.2

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.
Files changed (87) hide show
  1. package/build/components/display/avatar.d.ts +10 -0
  2. package/build/components/display/avatar.d.ts.map +1 -0
  3. package/build/components/display/avatar.js +11 -0
  4. package/build/components/display/avatar.js.map +1 -0
  5. package/build/components/display/avatar_group.d.ts +9 -0
  6. package/build/components/display/avatar_group.d.ts.map +1 -0
  7. package/build/components/display/avatar_group.js +11 -0
  8. package/build/components/display/avatar_group.js.map +1 -0
  9. package/build/components/display/heading.d.ts +8 -0
  10. package/build/components/display/heading.d.ts.map +1 -0
  11. package/build/components/display/heading.js +53 -0
  12. package/build/components/display/heading.js.map +1 -0
  13. package/build/components/display/image.d.ts +10 -3
  14. package/build/components/display/image.d.ts.map +1 -1
  15. package/build/components/display/image.js +7 -5
  16. package/build/components/display/image.js.map +1 -1
  17. package/build/components/display/index.d.ts +4 -1
  18. package/build/components/display/index.d.ts.map +1 -1
  19. package/build/components/display/index.js +4 -1
  20. package/build/components/display/index.js.map +1 -1
  21. package/build/components/display/text.d.ts +1 -1
  22. package/build/components/display/text.d.ts.map +1 -1
  23. package/build/components/display/text.js +7 -6
  24. package/build/components/display/text.js.map +1 -1
  25. package/build/components/error_boundary.d.ts +1 -1
  26. package/build/components/primitive/avatar_primitive.d.ts +39 -0
  27. package/build/components/primitive/avatar_primitive.d.ts.map +1 -0
  28. package/build/components/primitive/avatar_primitive.js +204 -0
  29. package/build/components/primitive/avatar_primitive.js.map +1 -0
  30. package/build/contexts/api_provider.d.ts.map +1 -1
  31. package/build/contexts/api_provider.js +3 -14
  32. package/build/contexts/api_provider.js.map +1 -1
  33. package/build/contexts/chat_context.js +2 -2
  34. package/build/contexts/chat_context.js.map +1 -1
  35. package/build/index.js.map +1 -1
  36. package/build/screens/display.d.ts.map +1 -1
  37. package/build/screens/display.js +51 -10
  38. package/build/screens/display.js.map +1 -1
  39. package/build/utils/api.d.ts +9 -0
  40. package/build/utils/api.d.ts.map +1 -0
  41. package/build/utils/api.js +36 -0
  42. package/build/utils/api.js.map +1 -0
  43. package/build/utils/platform_styles.d.ts +2 -0
  44. package/build/utils/platform_styles.d.ts.map +1 -0
  45. package/build/utils/platform_styles.js +7 -0
  46. package/build/utils/platform_styles.js.map +1 -0
  47. package/build/utils/space.d.ts +3 -0
  48. package/build/utils/space.d.ts.map +1 -0
  49. package/build/utils/space.js +22 -0
  50. package/build/utils/space.js.map +1 -0
  51. package/build/utils/theme.d.ts +2 -2
  52. package/build/utils/theme.d.ts.map +1 -1
  53. package/build/utils/theme.js +2 -2
  54. package/build/utils/theme.js.map +1 -1
  55. package/build/vendor/tapestry/alias_tokens_color_map.d.ts +55 -0
  56. package/build/vendor/tapestry/alias_tokens_color_map.d.ts.map +1 -0
  57. package/build/vendor/tapestry/{tapestry_alias_tokens_color_map.js → alias_tokens_color_map.js} +4 -2
  58. package/build/vendor/tapestry/alias_tokens_color_map.js.map +1 -0
  59. package/build/vendor/tapestry/tokens.d.ts +49 -35
  60. package/build/vendor/tapestry/tokens.d.ts.map +1 -1
  61. package/build/vendor/tapestry/tokens.js +18 -0
  62. package/build/vendor/tapestry/tokens.js.map +1 -1
  63. package/package.json +8 -9
  64. package/src/__mocks__/@react-native-async-storage/async-storage.js +3 -0
  65. package/src/__mocks__/react-native-device-info.js +3 -0
  66. package/src/__tests__/hooks/useTheme.tsx +37 -0
  67. package/src/__tests__/utils/space.tsx +60 -0
  68. package/src/components/display/avatar.tsx +23 -0
  69. package/src/components/display/avatar_group.tsx +21 -0
  70. package/src/components/display/heading.tsx +71 -0
  71. package/src/components/display/image.tsx +20 -8
  72. package/src/components/display/index.ts +4 -1
  73. package/src/components/display/text.tsx +7 -6
  74. package/src/components/primitive/avatar_primitive.tsx +374 -0
  75. package/src/contexts/api_provider.tsx +4 -17
  76. package/src/contexts/chat_context.tsx +2 -2
  77. package/src/index.tsx +0 -1
  78. package/src/screens/display.tsx +52 -13
  79. package/src/utils/api.ts +47 -0
  80. package/src/utils/platform_styles.ts +7 -0
  81. package/src/utils/space.ts +39 -0
  82. package/src/utils/theme.ts +3 -3
  83. package/src/vendor/tapestry/{tapestry_alias_tokens_color_map.ts → alias_tokens_color_map.ts} +8 -5
  84. package/src/vendor/tapestry/tokens.ts +25 -50
  85. package/build/vendor/tapestry/tapestry_alias_tokens_color_map.d.ts +0 -53
  86. package/build/vendor/tapestry/tapestry_alias_tokens_color_map.d.ts.map +0 -1
  87. package/build/vendor/tapestry/tapestry_alias_tokens_color_map.js.map +0 -1
@@ -0,0 +1,374 @@
1
+ import React, { createContext, useContext, useEffect, useState } from 'react'
2
+ import { StyleSheet, View, ViewProps } from 'react-native'
3
+ import { useTheme } from '../../hooks'
4
+ import { Image, ImageProps } from '../display/image'
5
+ import { Spinner } from '../display/spinner'
6
+
7
+ // =================================
8
+ // ====== Exports ==================
9
+ // =================================
10
+
11
+ const Avatar = {
12
+ Root: AvatarRoot,
13
+ Image: AvatarImage,
14
+ Presence: AvatarPresence,
15
+ Group: AvatarGroup,
16
+ GroupLoader: AvatarGroupLoader,
17
+ Mask: AvatarMask,
18
+ } as const
19
+
20
+ type AvatarComponents = {
21
+ Root: React.FC<AvatarRootProps>
22
+ Image: React.FC<AvatarImageProps>
23
+ Presence: React.FC<AvatarPresenceProps>
24
+ Group: React.FC<AvatarGroupProps>
25
+ GroupLoader: React.FC<Record<string, never>>
26
+ Mask: React.FC<AvatarMaskProps>
27
+ }
28
+
29
+ export default Avatar as AvatarComponents
30
+ export type {
31
+ AvatarImageProps,
32
+ AvatarPresenceProps,
33
+ AvatarGroupProps,
34
+ AvatarMaskProps,
35
+ AvatarRootProps,
36
+ }
37
+
38
+ // =================================
39
+ // ====== Constants & Types ========
40
+ // =================================
41
+
42
+ const AVATAR_SIZES = {
43
+ md: 'md',
44
+ lg: 'lg',
45
+ } as const
46
+
47
+ const AVATAR_PRESENCE_TYPES = {
48
+ online: 'online',
49
+ offline: 'offline',
50
+ } as const
51
+
52
+ // Progrmatically creates type unions
53
+ type AvatarSize = (typeof AVATAR_SIZES)[keyof typeof AVATAR_SIZES]
54
+ type AvatarPresenceType = (typeof AVATAR_PRESENCE_TYPES)[keyof typeof AVATAR_PRESENCE_TYPES]
55
+
56
+ const AVATAR_PX: Record<AvatarSize, number> = {
57
+ [AVATAR_SIZES.md]: 32,
58
+ [AVATAR_SIZES.lg]: 40,
59
+ }
60
+
61
+ const AVATAR_PRESENCE_PX: Record<AvatarSize, number> = {
62
+ [AVATAR_SIZES.md]: 12,
63
+ [AVATAR_SIZES.lg]: 14,
64
+ }
65
+
66
+ // =================================
67
+ // ====== Context ==================
68
+ // =================================
69
+
70
+ interface AvatarContextType {
71
+ size: AvatarSize
72
+ allImagesLoaded: boolean
73
+ setAllImagesLoaded: React.Dispatch<React.SetStateAction<boolean>>
74
+ }
75
+
76
+ const AvatarContext = createContext<AvatarContextType | null>(null)
77
+
78
+ function useAvatarContext() {
79
+ const context = useContext(AvatarContext)
80
+ if (!context) {
81
+ throw new Error('Avatar components must be used within Avatar.Root')
82
+ }
83
+ return context
84
+ }
85
+
86
+ // =================================
87
+ // ====== AvatarRoot ===============
88
+ // =================================
89
+
90
+ interface AvatarRootProps {
91
+ children: React.ReactNode
92
+ size?: AvatarSize
93
+ }
94
+
95
+ function AvatarRoot({ children, size = 'md' }: AvatarRootProps) {
96
+ const [allImagesLoaded, setAllImagesLoaded] = useState(false)
97
+ const styles = useStyles(size)
98
+
99
+ return (
100
+ <AvatarContext.Provider value={{ size, allImagesLoaded, setAllImagesLoaded }}>
101
+ <View style={styles.rootContainer}>{children}</View>
102
+ </AvatarContext.Provider>
103
+ )
104
+ }
105
+
106
+ AvatarRoot.displayName = 'Avatar.Root'
107
+
108
+ // =================================
109
+ // ====== AvatarMask ===============
110
+ // =================================
111
+
112
+ type AvatarMaskProps = ViewProps
113
+
114
+ function AvatarMask({ children, ...props }: AvatarMaskProps) {
115
+ const styles = useStyles()
116
+
117
+ return (
118
+ <View style={styles.mask} {...props}>
119
+ {children}
120
+ </View>
121
+ )
122
+ }
123
+
124
+ AvatarMask.displayName = 'Avatar.Mask'
125
+
126
+ // =================================
127
+ // ====== AvatarImage ============
128
+ // =================================
129
+
130
+ interface AvatarImageProps extends Omit<ImageProps, 'source'> {
131
+ sourceUri: string
132
+ }
133
+
134
+ function AvatarImage({ sourceUri, ...props }: AvatarImageProps) {
135
+ const { size } = useAvatarContext()
136
+
137
+ return <Image source={{ uri: sourceUri }} loaderSize={AVATAR_PX[size]} {...props} />
138
+ }
139
+
140
+ AvatarImage.displayName = 'Avatar.Image'
141
+
142
+ interface AvatarGroupImageProps {
143
+ sourceUri: string
144
+ style?: ImageProps['wrapperStyle']
145
+ onLoad?: () => void
146
+ }
147
+
148
+ function AvatarGroupImage({ sourceUri, style, onLoad }: AvatarGroupImageProps) {
149
+ return (
150
+ <Image
151
+ source={{ uri: sourceUri }}
152
+ loadingEnabled={false}
153
+ wrapperStyle={style}
154
+ onLoad={onLoad}
155
+ />
156
+ )
157
+ }
158
+
159
+ // =================================
160
+ // ====== AvatarGroup ============
161
+ // =================================
162
+
163
+ interface AvatarGroupProps {
164
+ sourceUris: string[]
165
+ }
166
+
167
+ function AvatarGroup({ sourceUris }: AvatarGroupProps) {
168
+ const styles = useStyles()
169
+ const { setAllImagesLoaded } = useAvatarContext()
170
+ const [loadingStatus, setLoadingStatus] = useState({
171
+ 0: false,
172
+ 1: false,
173
+ 2: false,
174
+ 3: false,
175
+ })
176
+ const displayUris = sourceUris.slice(0, 4)
177
+ const hasDisplayUris = displayUris.length > 0
178
+
179
+ const handleImageLoaded = index => {
180
+ setLoadingStatus(prev => ({
181
+ ...prev,
182
+ [index]: true,
183
+ }))
184
+ }
185
+
186
+ useEffect(() => {
187
+ const allImagesLoaded =
188
+ hasDisplayUris && displayUris.every((_, index) => loadingStatus[index] === true)
189
+
190
+ setAllImagesLoaded(allImagesLoaded)
191
+ }, [displayUris, hasDisplayUris, loadingStatus, setAllImagesLoaded])
192
+
193
+ if (!hasDisplayUris) {
194
+ return null
195
+ }
196
+
197
+ if (displayUris.length === 1) {
198
+ return <AvatarGroupImage sourceUri={displayUris[0]} onLoad={() => handleImageLoaded(0)} />
199
+ }
200
+
201
+ if (displayUris.length === 2) {
202
+ return (
203
+ <View style={styles.groupRow}>
204
+ <AvatarGroupImage
205
+ sourceUri={displayUris[0]}
206
+ style={styles.halfWidthFullHeight}
207
+ onLoad={() => handleImageLoaded(0)}
208
+ />
209
+ <AvatarGroupImage
210
+ sourceUri={displayUris[1]}
211
+ style={styles.halfWidthFullHeight}
212
+ onLoad={() => handleImageLoaded(1)}
213
+ />
214
+ </View>
215
+ )
216
+ }
217
+
218
+ if (displayUris.length === 3) {
219
+ return (
220
+ <View style={styles.groupColumn}>
221
+ <View style={styles.groupRow}>
222
+ <AvatarGroupImage
223
+ sourceUri={displayUris[0]}
224
+ style={styles.halfWidthFullHeight}
225
+ onLoad={() => handleImageLoaded(0)}
226
+ />
227
+ <View style={styles.groupColumn}>
228
+ <AvatarGroupImage
229
+ sourceUri={displayUris[1]}
230
+ style={styles.fullWidthHalfHeight}
231
+ onLoad={() => handleImageLoaded(1)}
232
+ />
233
+ <AvatarGroupImage
234
+ sourceUri={displayUris[2]}
235
+ style={styles.fullWidthHalfHeight}
236
+ onLoad={() => handleImageLoaded(2)}
237
+ />
238
+ </View>
239
+ </View>
240
+ </View>
241
+ )
242
+ }
243
+
244
+ return (
245
+ <View style={styles.groupColumn}>
246
+ <View style={styles.groupRow}>
247
+ <AvatarGroupImage
248
+ sourceUri={displayUris[0]}
249
+ style={styles.halfWidthFullHeight}
250
+ onLoad={() => handleImageLoaded(0)}
251
+ />
252
+ <AvatarGroupImage
253
+ sourceUri={displayUris[1]}
254
+ style={styles.halfWidthFullHeight}
255
+ onLoad={() => handleImageLoaded(1)}
256
+ />
257
+ </View>
258
+ <View style={styles.groupRow}>
259
+ <AvatarGroupImage
260
+ sourceUri={displayUris[2]}
261
+ style={styles.halfWidthFullHeight}
262
+ onLoad={() => handleImageLoaded(2)}
263
+ />
264
+ <AvatarGroupImage
265
+ sourceUri={displayUris[3]}
266
+ style={styles.halfWidthFullHeight}
267
+ onLoad={() => handleImageLoaded(3)}
268
+ />
269
+ </View>
270
+ </View>
271
+ )
272
+ }
273
+
274
+ AvatarGroup.displayName = 'Avatar.Group'
275
+
276
+ // =================================
277
+ // ====== AvatarGroupLoader =========
278
+ // =================================
279
+
280
+ function AvatarGroupLoader() {
281
+ const { size, allImagesLoaded } = useAvatarContext()
282
+ const styles = useStyles(size)
283
+
284
+ if (allImagesLoaded) return null
285
+
286
+ return (
287
+ <View style={styles.groupLoader}>
288
+ <Spinner size={AVATAR_PX[size]} />
289
+ </View>
290
+ )
291
+ }
292
+
293
+ AvatarGroupLoader.displayName = 'Avatar.GroupLoader'
294
+
295
+ // =================================
296
+ // ====== AvatarPresence =========
297
+ // =================================
298
+
299
+ interface AvatarPresenceProps extends ViewProps {
300
+ presence: AvatarPresenceType
301
+ }
302
+
303
+ function AvatarPresence({ presence, ...props }: AvatarPresenceProps) {
304
+ const { size } = useAvatarContext()
305
+ const styles = useStyles(size, presence)
306
+
307
+ return <View style={styles.presence} {...props} />
308
+ }
309
+
310
+ AvatarPresence.displayName = 'Avatar.Presence'
311
+
312
+ // =================================
313
+ // ====== Styles ===================
314
+ // =================================
315
+
316
+ const useStyles = (size: AvatarSize = 'md', presence: AvatarPresenceType = 'offline') => {
317
+ const { colors } = useTheme()
318
+ const PRESENCE_COLOR = {
319
+ online: colors.fillColorInteractionOnlineDefault,
320
+ offline: colors.iconColorDefaultDisabled,
321
+ }
322
+ const presenceDiameter = AVATAR_PRESENCE_PX[size]
323
+ const avatarDiameter = AVATAR_PX[size]
324
+ const groupGap = 1
325
+
326
+ return StyleSheet.create({
327
+ rootContainer: {
328
+ height: avatarDiameter,
329
+ width: avatarDiameter,
330
+ },
331
+ mask: {
332
+ borderRadius: avatarDiameter,
333
+ overflow: 'hidden',
334
+ width: '100%',
335
+ height: '100%',
336
+ },
337
+ presence: {
338
+ height: presenceDiameter,
339
+ width: presenceDiameter,
340
+ backgroundColor: PRESENCE_COLOR[presence],
341
+ borderColor: colors.fillColorNeutral100Inverted,
342
+ borderWidth: 2,
343
+ borderRadius: presenceDiameter,
344
+ position: 'absolute',
345
+ bottom: -1,
346
+ right: -1,
347
+ },
348
+ groupLoader: {
349
+ position: 'absolute',
350
+ top: 0,
351
+ left: 0,
352
+ borderRadius: avatarDiameter,
353
+ width: avatarDiameter,
354
+ height: avatarDiameter,
355
+ },
356
+ groupColumn: {
357
+ flex: 1,
358
+ gap: groupGap,
359
+ },
360
+ groupRow: {
361
+ flexDirection: 'row',
362
+ flex: 1,
363
+ gap: groupGap,
364
+ },
365
+ halfWidthFullHeight: {
366
+ width: '50%',
367
+ height: '100%',
368
+ },
369
+ fullWidthHalfHeight: {
370
+ width: '100%',
371
+ height: '50%',
372
+ },
373
+ })
374
+ }
@@ -1,7 +1,9 @@
1
+ import { JSONAPIResponse } from '@planningcenter/chat-core'
1
2
  import { QueryClient, QueryClientProvider, QueryKey } from '@tanstack/react-query'
2
3
  import React, { useEffect } from 'react'
3
4
  import { ViewProps } from 'react-native'
4
5
  import { OAuthToken } from '../types'
6
+ import apiRequest from '../utils/api'
5
7
  import { ENV, session } from '../utils/session'
6
8
 
7
9
  let handleTokenExpired: () => void
@@ -13,13 +15,8 @@ const defaultQueryFn = ({ queryKey }: { queryKey: QueryKey }) => {
13
15
 
14
16
  const url = `${session.baseUrl}${queryKey[0]}`
15
17
 
16
- return fetch(url, {
17
- headers: {
18
- Authorization: `Bearer ${session.token?.access_token}`,
19
- },
20
- })
21
- .then(validateResponse)
22
- .then(response => response.json())
18
+ return apiRequest<JSONAPIResponse>(url)
19
+ .then(r => r.json)
23
20
  .catch(error => {
24
21
  if (error.message === 'Token expired') {
25
22
  handleTokenExpired()
@@ -52,13 +49,3 @@ export function ApiProvider({
52
49
 
53
50
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
54
51
  }
55
-
56
- const validateResponse = (response: Response) => {
57
- const isExpired = response.status === 401
58
-
59
- if (isExpired) {
60
- throw new Error('Token expired')
61
- }
62
-
63
- return response
64
- }
@@ -3,7 +3,7 @@ import React, { createContext, useMemo } from 'react'
3
3
  import { ColorSchemeName, useColorScheme } from 'react-native'
4
4
  import { DeepPartial, OAuthToken } from '../types'
5
5
  import { defaultTheme, DefaultTheme } from '../utils/theme'
6
- import { tapestryAliasTokensColorMap } from '../vendor/tapestry/tapestry_alias_tokens_color_map'
6
+ import { aliasTokensColorMap } from '../vendor/tapestry/alias_tokens_color_map'
7
7
  import { ApiProvider } from './api_provider'
8
8
 
9
9
  type ContextValue = {
@@ -51,7 +51,7 @@ export const useCreateChatTheme = ({
51
51
  ...merge({}, defaultTheme(colorScheme), customTheme),
52
52
  colors: {
53
53
  ...merge({}, defaultTheme(colorScheme).colors, customTheme?.colors),
54
- ...tapestryAliasTokensColorMap[colorScheme],
54
+ ...aliasTokensColorMap[colorScheme],
55
55
  },
56
56
  }
57
57
  }, [colorScheme, customTheme])
package/src/index.tsx CHANGED
@@ -3,4 +3,3 @@ export * from './contexts'
3
3
  export * from './hooks'
4
4
  export * from './screens'
5
5
  export * from './utils'
6
-
@@ -1,7 +1,27 @@
1
1
  import React from 'react'
2
2
  import { ScrollView, StyleSheet, View } from 'react-native'
3
3
  import { useTheme } from '../hooks'
4
- import { Image, Spinner, Text } from '../components/display'
4
+ import { Avatar, AvatarGroup, Heading, Image, Spinner, Text } from '../components/display'
5
+ import { space } from '../utils/space'
6
+
7
+ const URL = {
8
+ image: 'https://picsum.photos/seed/picsum/200',
9
+ broken: 'https://broken.url',
10
+ avatar: 'https://i.pravatar.cc/200?img=22',
11
+ avatar_fallback: 'https://avatars.planningcenteronline.com/uploads/initials/PR.png',
12
+ two_avatars: ['https://i.pravatar.cc/200?img=22', 'https://i.pravatar.cc/200?img=23'],
13
+ three_avatars: [
14
+ 'https://i.pravatar.cc/200?img=22',
15
+ 'https://i.pravatar.cc/200?img=23',
16
+ 'https://i.pravatar.cc/200?img=24',
17
+ ],
18
+ four_avatars: [
19
+ 'https://i.pravatar.cc/200?img=30',
20
+ 'https://i.pravatar.cc/200?img=29',
21
+ 'https://i.pravatar.cc/200?img=28',
22
+ 'https://i.pravatar.cc/200?img=27',
23
+ ],
24
+ }
5
25
 
6
26
  export function DisplayScreen() {
7
27
  const styles = useStyles()
@@ -13,13 +33,26 @@ export function DisplayScreen() {
13
33
  <Spinner size={24} />
14
34
  </View>
15
35
  <View style={styles.row}>
16
- <Image source={{ uri: 'https://broken.url' }} style={styles.image} />
17
- <Image
18
- source={{
19
- uri: 'https://picsum.photos/seed/picsum/200',
20
- }}
21
- style={styles.image}
22
- />
36
+ <Image source={{ uri: URL.broken }} style={styles.image} />
37
+ <Image source={{ uri: URL.image }} style={styles.image} />
38
+ </View>
39
+ <View style={styles.row}>
40
+ <Avatar sourceUri={URL.broken} />
41
+ <Avatar size="md" sourceUri={URL.avatar_fallback} />
42
+ <Avatar sourceUri={URL.avatar} />
43
+ </View>
44
+ <View style={styles.row}>
45
+ <Avatar presence="offline" sourceUri={URL.broken} />
46
+ <Avatar presence="online" size="md" sourceUri={URL.avatar_fallback} />
47
+ <Avatar presence="offline" sourceUri={URL.avatar} />
48
+ </View>
49
+ <View style={styles.row}>
50
+ <AvatarGroup sourceUris={[URL.broken]} />
51
+ <AvatarGroup sourceUris={[URL.broken, URL.broken, ...URL.two_avatars]} />
52
+ <AvatarGroup sourceUris={[URL.avatar]} />
53
+ <AvatarGroup sourceUris={URL.two_avatars} />
54
+ <AvatarGroup sourceUris={URL.three_avatars} />
55
+ <AvatarGroup sourceUris={URL.four_avatars} />
23
56
  </View>
24
57
  <View style={styles.row}>
25
58
  <Text>Plain text</Text>
@@ -27,6 +60,12 @@ export function DisplayScreen() {
27
60
  <Text variant="tertiary">Tertiary</Text>
28
61
  <Text variant="footnote">Footnote</Text>
29
62
  </View>
63
+ <View style={styles.row}>
64
+ <Heading>Heading 1</Heading>
65
+ <Heading variant="h2">Heading 2</Heading>
66
+ <Heading variant="h3">Heading 3</Heading>
67
+ <Heading variant="h4">Heading 4</Heading>
68
+ </View>
30
69
  </View>
31
70
  </ScrollView>
32
71
  )
@@ -36,19 +75,19 @@ const useStyles = () => {
36
75
  const { colors } = useTheme()
37
76
 
38
77
  return StyleSheet.create({
39
- scrollView: { flex: 1, backgroundColor: colors.fillColorNeutral090 },
40
- container: { gap: 16, padding: 24 },
78
+ scrollView: { flex: 1, backgroundColor: colors.fillColorNeutral100Inverted },
79
+ container: { gap: space(2), padding: space(3) },
41
80
  listItem: { color: colors.fillColorNeutral020 },
42
81
  row: {
43
- gap: 16,
82
+ gap: space(2),
44
83
  flexDirection: 'row',
45
84
  alignItems: 'center',
46
85
  justifyContent: 'center',
47
86
  flexWrap: 'wrap',
48
87
  },
49
- column: { gap: 32 },
88
+ column: { gap: space(4) },
50
89
  spinnerContainer: {
51
- height: 20,
90
+ height: space(2.5),
52
91
  },
53
92
  image: {
54
93
  width: 100,
@@ -0,0 +1,47 @@
1
+ import DeviceInfo from 'react-native-device-info'
2
+ import { session } from './session'
3
+
4
+ const brand = DeviceInfo.getBrand()
5
+ const model = DeviceInfo.getModel()
6
+ const systemName = DeviceInfo.getSystemName()
7
+ const systemVersion = DeviceInfo.getSystemVersion()
8
+ const readableVersion = DeviceInfo.getReadableVersion()
9
+ const appName = DeviceInfo.getApplicationName()
10
+
11
+ export default function apiRequest<T = unknown>(
12
+ url: string,
13
+ { method = 'GET', data = null } = {}
14
+ ): Promise<{ json: T; ok: boolean; response: Response }> {
15
+ const options: RequestInit = {
16
+ headers: {
17
+ Accept: 'application/vnd.api+json',
18
+ 'Content-Type': 'application/json',
19
+ 'User-Agent': `${appName}/${readableVersion} (${brand}, ${model}, ${systemName}, ${systemVersion})`,
20
+ Authorization: `Bearer ${session.token?.access_token}`,
21
+ },
22
+ method,
23
+ }
24
+
25
+ if (data && method !== 'GET') {
26
+ options.body = JSON.stringify(data)
27
+ }
28
+
29
+ return fetch(url, options)
30
+ .then(validateResponse)
31
+ .then(response =>
32
+ response
33
+ .json()
34
+ .then(json => ({ json: json as T, ok: response.ok, response }))
35
+ .catch(() => ({ json: null as T, ok: response.ok, response }))
36
+ )
37
+ }
38
+
39
+ const validateResponse = (response: Response) => {
40
+ const isExpired = response.status === 401
41
+
42
+ if (isExpired) {
43
+ throw new Error('Token expired')
44
+ }
45
+
46
+ return response
47
+ }
@@ -0,0 +1,7 @@
1
+ import { Platform } from 'react-native'
2
+ import { tokens } from '../vendor/tapestry/tokens'
3
+
4
+ export const platformFontWeightBold = Platform.select({
5
+ ios: tokens.fontWeightSemiBold,
6
+ android: tokens.fontWeightBold,
7
+ })
@@ -0,0 +1,39 @@
1
+ import { tokens } from '../vendor/tapestry/tokens'
2
+
3
+ export type SpacingValues =
4
+ | 0.25
5
+ | 0.5
6
+ | 1
7
+ | 1.5
8
+ | 2
9
+ | 2.5
10
+ | 3
11
+ | 3.5
12
+ | 4
13
+ | 4.5
14
+ | 5
15
+ | 5.5
16
+ | 6
17
+ | 6.5
18
+ | 7
19
+ | 7.5
20
+
21
+ export function space(value: SpacingValues): number {
22
+ if (value === 0.25) return tokens.spacingFourth
23
+ if (value === 0.5) return tokens.spacingHalf
24
+ if (value < 1 || value > 7.5) return handleInvalidSpace(value)
25
+
26
+ // Reject fractional values that are not 0 or 0.5
27
+ const wholeValue = Math.floor(value)
28
+ const fractionalValue = value % 1
29
+ if (fractionalValue !== 0 && fractionalValue !== 0.5) return handleInvalidSpace(value)
30
+
31
+ // Deliver a whole value or add a half spacing token to it
32
+ const remainderValue = fractionalValue === 0.5 ? tokens.spacingHalf : 0
33
+ return tokens[`spacing${wholeValue}`] + remainderValue
34
+ }
35
+
36
+ function handleInvalidSpace(value: number) {
37
+ console.warn(`Invalid space value: ${value} — Must be a whole or half number between 1–7.`)
38
+ return 0
39
+ }
@@ -1,10 +1,10 @@
1
1
  import { TextStyle, ViewStyle, ColorSchemeName } from 'react-native'
2
2
  import { tokens } from '../vendor/tapestry/tokens'
3
- import { tapestryAliasTokensColorMap } from '../vendor/tapestry/tapestry_alias_tokens_color_map'
3
+ import { aliasTokensColorMap } from '../vendor/tapestry/alias_tokens_color_map'
4
4
 
5
5
  export interface ChatTheme extends DefaultTheme {
6
6
  colors: DefaultTheme['colors'] &
7
- (typeof tapestryAliasTokensColorMap.light | typeof tapestryAliasTokensColorMap.dark)
7
+ (typeof aliasTokensColorMap.light | typeof aliasTokensColorMap.dark)
8
8
  }
9
9
 
10
10
  /** =============================================
@@ -37,7 +37,7 @@ export const defaultTheme = (colorScheme: ColorSchemeName): DefaultTheme => {
37
37
  temporaryProductBadge: {
38
38
  container: {
39
39
  paddingHorizontal: tokens.spacing1,
40
- backgroundColor: tapestryAliasTokensColorMap[scheme].fillColorNeutral070,
40
+ backgroundColor: aliasTokensColorMap[scheme].fillColorNeutral070,
41
41
  borderWidth: tokens.borderSizeDefault,
42
42
  },
43
43
  text: {