@planningcenter/chat-react-native 1.4.0-rc.1 → 1.4.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 CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "1.4.0-rc.1",
3
+ "version": "1.4.0",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
7
  "files": [
8
- "build"
8
+ "build",
9
+ "src"
9
10
  ],
10
11
  "sideEffects": false,
11
12
  "scripts": {
@@ -16,7 +17,7 @@
16
17
  "prepublishOnly": "expo-module prepublishOnly"
17
18
  },
18
19
  "dependencies": {
19
- "@planningcenter/chat-core": "^1.4.0-rc.1"
20
+ "@planningcenter/chat-core": "^1.4.0"
20
21
  },
21
22
  "peerDependencies": {
22
23
  "@react-navigation/elements": "*",
@@ -37,5 +38,5 @@
37
38
  "prettier": "^3.4.2",
38
39
  "react-native": "0.74.5"
39
40
  },
40
- "gitHead": "0dc211ff36b02633298f056dc893ddf08daecdff"
41
+ "gitHead": "6bcacc755d471a8f03b890abd733d5805f643aa4"
41
42
  }
@@ -0,0 +1,5 @@
1
+ describe('Chat', () => {
2
+ it('should do maths', () => {
3
+ expect(1 + 1).toBe(2)
4
+ })
5
+ })
@@ -0,0 +1,46 @@
1
+ import { OAuthToken } from '../types'
2
+ import { Session } from '../utils/session'
3
+
4
+ describe('Session', () => {
5
+ describe('constructor', () => {
6
+ it('should track the environment', () => {
7
+ const session = new Session()
8
+ expect(session.env).toBe('development')
9
+ expect(session.isAuthenticated).toBe(false)
10
+ })
11
+
12
+ it('should track the token', () => {
13
+ const token: OAuthToken = {
14
+ access_token: 'access_token',
15
+ refresh_token: 'refresh_token',
16
+ token_type: undefined,
17
+ created_at: 0,
18
+ expires_in: undefined,
19
+ scope: '',
20
+ }
21
+ const session = new Session({ token })
22
+ expect(session.token).toEqual(token)
23
+ expect(session.isAuthenticated).toEqual(true)
24
+ })
25
+ })
26
+
27
+ describe('urls', () => {
28
+ it('should return the correct base url', () => {
29
+ const session = new Session({ env: 'production' })
30
+ expect(session.baseUrl).toBe('https://api.planningcenteronline.com')
31
+ session.env = 'development'
32
+ expect(session.baseUrl).toBe('http://api.pco.test')
33
+ session.env = 'staging'
34
+ expect(session.baseUrl).toBe('https://api-staging.planningcenteronline.com')
35
+ })
36
+
37
+ it('should return the correct upload url', () => {
38
+ const session = new Session({ env: 'production' })
39
+ expect(session.uploadUrl).toBe('https://upload.planningcenteronline.com/v2/files')
40
+ session.env = 'development'
41
+ expect(session.uploadUrl).toBe('https://upload.planningcenteronline.com/v2/files')
42
+ session.env = 'staging'
43
+ expect(session.uploadUrl).toBe('https://upload-staging.planningcenteronline.com/v2/files')
44
+ })
45
+ })
46
+ })
@@ -0,0 +1,94 @@
1
+ import { ConversationResource, JSONAPICollection } from '@planningcenter/chat-core'
2
+ import { useNavigation } from '@react-navigation/native'
3
+ import { QueryErrorResetBoundary, useSuspenseQuery } from '@tanstack/react-query'
4
+ import React, { Suspense, useContext } from 'react'
5
+ import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'
6
+ import { ChatContext } from '../contexts/chat_context'
7
+ import { useTheme } from '../hooks'
8
+ import { Image, Spinner } from './display'
9
+ import ErrorBoundary from './error_boundary'
10
+
11
+ type ConversationsResponse = JSONAPICollection<ConversationResource>
12
+
13
+ export function Conversations() {
14
+ const { token } = useContext(ChatContext)
15
+
16
+ if (!token) return null
17
+
18
+ return (
19
+ <QueryErrorResetBoundary>
20
+ {({ reset }) => (
21
+ <ErrorBoundary onReset={reset}>
22
+ <Suspense fallback={<Text>loading...</Text>}>
23
+ <Loaded />
24
+ </Suspense>
25
+ </ErrorBoundary>
26
+ )}
27
+ </QueryErrorResetBoundary>
28
+ )
29
+ }
30
+
31
+ const Loaded = () => {
32
+ const styles = useStyles()
33
+ const { data: conversations } = useSuspenseQuery<ConversationsResponse>({
34
+ queryKey: ['/chat/v2/me/conversations'],
35
+ })
36
+ const navigation = useNavigation()
37
+
38
+ return (
39
+ <FlatList
40
+ data={conversations?.data}
41
+ contentContainerStyle={styles.container}
42
+ style={styles.scrollView}
43
+ ListEmptyComponent={<Text>No conversations found</Text>}
44
+ ListHeaderComponent={
45
+ <View style={styles.column}>
46
+ <Text style={styles.foo}>Display Components</Text>
47
+ <View style={[styles.row, styles.spinnerContainer]}>
48
+ <Spinner size={24} />
49
+ </View>
50
+ <View style={styles.row}>
51
+ <Image source={{ uri: 'https://broken.url' }} style={styles.image} />
52
+ <Image
53
+ source={{
54
+ uri: 'https://picsum.photos/seed/picsum/200',
55
+ }}
56
+ style={styles.image}
57
+ />
58
+ </View>
59
+ <Text style={styles.foo}>Conversations</Text>
60
+ </View>
61
+ }
62
+ renderItem={({ item }) => (
63
+ <Pressable onPress={() => navigation.navigate('Settings')}>
64
+ <Text style={styles.listItem}>{item.attributes.title}</Text>
65
+ </Pressable>
66
+ )}
67
+ />
68
+ )
69
+ }
70
+
71
+ const useStyles = () => {
72
+ const { colors } = useTheme()
73
+
74
+ return StyleSheet.create({
75
+ scrollView: { flex: 1, backgroundColor: colors.fillColorNeutral090 },
76
+ container: { gap: 8, padding: 16 },
77
+ foo: { fontSize: 24, color: colors.testColor },
78
+ listItem: { color: colors.fillColorNeutral020 },
79
+ row: {
80
+ gap: 16,
81
+ flexDirection: 'row',
82
+ alignItems: 'center',
83
+ justifyContent: 'center',
84
+ },
85
+ column: { gap: 16 },
86
+ spinnerContainer: {
87
+ height: 20,
88
+ },
89
+ image: {
90
+ width: 100,
91
+ height: 100,
92
+ },
93
+ })
94
+ }
@@ -0,0 +1,88 @@
1
+ import { noop } from 'lodash'
2
+ import React, { useState } from 'react'
3
+ import {
4
+ AnimatableNumericValue,
5
+ DimensionValue,
6
+ Image as ReactNativeImage,
7
+ ImageProps as RNImageProps,
8
+ StyleSheet,
9
+ View,
10
+ ViewStyle,
11
+ } from 'react-native'
12
+ import { useTheme } from '../../hooks'
13
+ import { Spinner } from './spinner'
14
+
15
+ interface ImageProps extends RNImageProps {
16
+ /**
17
+ * Should the image show the loading indicator by default.
18
+ */
19
+ defaultLoading?: boolean
20
+ /**
21
+ * Size of the loading spinner.
22
+ */
23
+ loaderSize?: number
24
+ /**
25
+ * Style object for the preload background.
26
+ */
27
+ loadingBackgroundStyles?: ViewStyle
28
+ }
29
+
30
+ export function Image({
31
+ source,
32
+ onLoad = noop,
33
+ defaultLoading = true,
34
+ loaderSize = 24,
35
+ loadingBackgroundStyles,
36
+ style,
37
+ ...restProps
38
+ }: ImageProps) {
39
+ const [loading, setLoading] = useState(defaultLoading)
40
+
41
+ const imageStyles = StyleSheet.flatten(style)
42
+ const { width = '100%', height = '100%', borderRadius = 0 } = imageStyles || {}
43
+ const styles = useStyles(width, height, borderRadius)
44
+
45
+ const handleOnLoad = (event: any) => {
46
+ setLoading(false)
47
+ onLoad?.(event)
48
+ }
49
+
50
+ return (
51
+ <View>
52
+ <ReactNativeImage
53
+ style={[styles.image, imageStyles]}
54
+ onLoad={handleOnLoad}
55
+ source={source}
56
+ {...restProps}
57
+ />
58
+ {loading && (
59
+ <View style={[styles.loadingBackground, loadingBackgroundStyles]}>
60
+ <Spinner size={loaderSize} />
61
+ </View>
62
+ )}
63
+ </View>
64
+ )
65
+ }
66
+
67
+ const useStyles = (
68
+ width: DimensionValue,
69
+ height: DimensionValue,
70
+ borderRadius: AnimatableNumericValue
71
+ ) => {
72
+ const { colors } = useTheme()
73
+
74
+ return StyleSheet.create({
75
+ loadingBackground: {
76
+ position: 'absolute',
77
+ top: 0,
78
+ left: 0,
79
+ backgroundColor: colors.fillColorNeutral070,
80
+ borderRadius,
81
+ width,
82
+ height,
83
+ },
84
+ image: {
85
+ backgroundColor: 'transparent',
86
+ },
87
+ })
88
+ }
@@ -0,0 +1,2 @@
1
+ export { Spinner } from './spinner'
2
+ export { Image } from './image'
@@ -0,0 +1,97 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+ import { Animated, Easing, StyleSheet, View } from 'react-native'
3
+ import { useTheme } from '../../hooks'
4
+
5
+ interface SpinnerProps {
6
+ /**
7
+ * Size of the spinner in px
8
+ * */
9
+ size?: number
10
+ }
11
+
12
+ export function Spinner({ size = 20 }: SpinnerProps) {
13
+ const rotation = useRef(new Animated.Value(0)).current
14
+
15
+ const animation = Animated.loop(
16
+ Animated.timing(rotation, {
17
+ toValue: 1,
18
+ duration: 1000,
19
+ easing: Easing.linear,
20
+ useNativeDriver: true,
21
+ })
22
+ )
23
+
24
+ const rotateValue = rotation.interpolate({
25
+ inputRange: [0, 1],
26
+ outputRange: ['0deg', '360deg'],
27
+ })
28
+
29
+ const styles = useStyles(rotateValue, size)
30
+
31
+ useEffect(() => {
32
+ animation.start()
33
+ return () => animation.stop()
34
+ }, [animation])
35
+
36
+ useEffect(() => () => rotation.setValue(0), [rotation])
37
+
38
+ return (
39
+ <View style={styles.container}>
40
+ <Animated.View style={styles.animatedContainer}>
41
+ <View style={styles.clipping}>
42
+ <View style={[styles.circle, styles.spinner]} />
43
+ </View>
44
+ <View style={[styles.circle, styles.track]} />
45
+ </Animated.View>
46
+ </View>
47
+ )
48
+ }
49
+
50
+ const useStyles = (rotate: Animated.AnimatedInterpolation<string | number>, size: number) => {
51
+ const { colors } = useTheme()
52
+
53
+ return StyleSheet.create({
54
+ container: {
55
+ width: '100%',
56
+ height: '100%',
57
+ position: 'absolute',
58
+ top: 0,
59
+ left: 0,
60
+ zIndex: 200,
61
+ justifyContent: 'center',
62
+ alignItems: 'center',
63
+ opacity: 0.7,
64
+ },
65
+ animatedContainer: {
66
+ width: size,
67
+ height: size,
68
+ borderRadius: size / 2,
69
+ transform: [{ rotate }],
70
+ },
71
+ circle: {
72
+ width: size,
73
+ height: size,
74
+ borderRadius: size / 2,
75
+ borderStyle: 'solid',
76
+ borderWidth: 3,
77
+ },
78
+ spinner: {
79
+ position: 'absolute',
80
+ top: 0,
81
+ left: 0,
82
+ borderColor: colors.fillColorNeutral020,
83
+ },
84
+ track: {
85
+ borderColor: colors.fillColorNeutral050Base,
86
+ },
87
+ clipping: {
88
+ position: 'absolute',
89
+ top: 0,
90
+ left: 0,
91
+ width: size / 2,
92
+ height: size / 2,
93
+ overflow: 'hidden',
94
+ zIndex: 200,
95
+ },
96
+ })
97
+ }
@@ -0,0 +1,27 @@
1
+ import React, { PropsWithChildren } from 'react'
2
+ import { Text } from 'react-native'
3
+
4
+ class ErrorBoundary extends React.Component<PropsWithChildren<{ onReset?: () => void }>> {
5
+ state = {
6
+ error: null,
7
+ unsubscriber: () => {},
8
+ }
9
+
10
+ componentDidCatch(error: any) {
11
+ this.handleError(error)
12
+ }
13
+
14
+ handleError(error: any) {
15
+ this.setState({ error })
16
+ }
17
+
18
+ render() {
19
+ if (this.state.error) {
20
+ return <Text>{JSON.stringify(this.state.error, null, 2)}</Text>
21
+ } else {
22
+ return this.props.children
23
+ }
24
+ }
25
+ }
26
+
27
+ export default ErrorBoundary
@@ -0,0 +1,60 @@
1
+ import { QueryClient, QueryClientProvider, QueryKey } from '@tanstack/react-query'
2
+ import React from 'react'
3
+ import { ViewProps } from 'react-native'
4
+ import { OAuthToken } from '../types'
5
+ import { ENV, session } from '../utils/session'
6
+
7
+ let handleTokenExpired: () => void
8
+
9
+ const defaultQueryFn = ({ queryKey }: { queryKey: QueryKey }) => {
10
+ if (!session.token) {
11
+ throw new Error('No token present')
12
+ }
13
+
14
+ const url = `${session.baseUrl}${queryKey[0]}`
15
+
16
+ return fetch(url, {
17
+ headers: {
18
+ Authorization: `Bearer ${session.token?.access_token}`,
19
+ },
20
+ })
21
+ .then(validateResponse)
22
+ .then(response => response.json())
23
+ .catch(error => {
24
+ if (error.message === 'Token expired') {
25
+ handleTokenExpired()
26
+ }
27
+ return null
28
+ })
29
+ }
30
+
31
+ export const queryClient = new QueryClient({
32
+ defaultOptions: {
33
+ queries: {
34
+ queryFn: defaultQueryFn,
35
+ },
36
+ },
37
+ })
38
+
39
+ export function ApiProvider({
40
+ children,
41
+ env = 'production',
42
+ token,
43
+ onTokenExpired,
44
+ }: ViewProps & { env?: ENV; token?: OAuthToken; onTokenExpired: () => void }) {
45
+ session.env = env
46
+ session.token = token
47
+ handleTokenExpired = onTokenExpired
48
+
49
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
50
+ }
51
+
52
+ const validateResponse = (response: Response) => {
53
+ const isExpired = response.status === 401
54
+
55
+ if (isExpired) {
56
+ throw new Error('Token expired')
57
+ }
58
+
59
+ return response
60
+ }
@@ -0,0 +1,29 @@
1
+ import React, { createContext } from 'react'
2
+ import { OAuthToken } from '../types'
3
+ import { ApiProvider } from './api_provider'
4
+
5
+ type ContextValue = {
6
+ token?: OAuthToken
7
+ onTokenExpired: () => void
8
+ theme: any
9
+ env?: 'production' | 'staging' | 'development'
10
+ }
11
+
12
+ export const ChatContext = createContext<ContextValue>({
13
+ theme: undefined,
14
+ token: undefined,
15
+ env: undefined,
16
+ onTokenExpired: () => {},
17
+ })
18
+
19
+ export function ChatProvider({ children, value }: { children: any; value: ContextValue }) {
20
+ if (!Object.keys(value.token || {}).length) return null
21
+
22
+ return (
23
+ <ChatContext.Provider value={value}>
24
+ <ApiProvider env={value.env} token={value.token} onTokenExpired={value.onTokenExpired}>
25
+ {children}
26
+ </ApiProvider>
27
+ </ChatContext.Provider>
28
+ )
29
+ }
@@ -0,0 +1,2 @@
1
+ export { useCreateChatTheme } from './use_create_chat_theme'
2
+ export { useTheme } from './use_theme'
@@ -0,0 +1,41 @@
1
+ import AsyncStorage from '@react-native-async-storage/async-storage'
2
+ import { useSuspenseQuery } from '@tanstack/react-query'
3
+ import { useCallback } from 'react'
4
+
5
+ const cacheKeyGenerator = (key: string) => [`AsyncStorageResource:${key}`]
6
+
7
+ type SetValue<TCacheData> = (_itemValue?: TCacheData | null) => void
8
+
9
+ export function useAsyncStorage<TCacheData>(
10
+ key: string,
11
+ initialValue: TCacheData
12
+ ): [TCacheData, SetValue<TCacheData>] {
13
+ const cacheKey = cacheKeyGenerator(key)
14
+ const { data: value, refetch } = useSuspenseQuery<TCacheData>({
15
+ queryKey: cacheKey,
16
+ queryFn: () =>
17
+ AsyncStorage.getItem(key).then(storedValue => {
18
+ if (!storedValue) return initialValue
19
+
20
+ try {
21
+ return JSON.parse(storedValue)
22
+ } catch {
23
+ return storedValue
24
+ }
25
+ }),
26
+ })
27
+
28
+ const setValue: SetValue<TCacheData> = useCallback(
29
+ itemValue => {
30
+ if (itemValue === null || itemValue === undefined) {
31
+ AsyncStorage.removeItem(key)
32
+ } else {
33
+ AsyncStorage.setItem(key, JSON.stringify(itemValue))
34
+ }
35
+ refetch()
36
+ },
37
+ [key, refetch]
38
+ )
39
+
40
+ return [value || initialValue, setValue]
41
+ }
@@ -0,0 +1,31 @@
1
+ import { useMemo } from 'react'
2
+ import { useColorScheme, ColorSchemeName } from 'react-native'
3
+ import { defaultTheme, DefaultTheme } from '../utils/theme'
4
+ import { DeepPartial } from '../types'
5
+ import { tapestryAliasTokensColorMap } from '../vendor/tapestry/tapestry_alias_tokens_color_map'
6
+ import { merge } from 'lodash'
7
+
8
+ interface CreateChatThemeProps {
9
+ theme?: DeepPartial<DefaultTheme>
10
+ colorScheme?: ColorSchemeName
11
+ }
12
+
13
+ export const useCreateChatTheme = ({
14
+ theme: customTheme = {},
15
+ colorScheme: appColorScheme,
16
+ }: CreateChatThemeProps) => {
17
+ const internalColorScheme = useColorScheme() || 'light'
18
+ const colorScheme = appColorScheme || internalColorScheme
19
+
20
+ const memoizedTheme = useMemo(() => {
21
+ return {
22
+ ...merge({}, defaultTheme(colorScheme), customTheme),
23
+ colors: {
24
+ ...merge({}, defaultTheme(colorScheme).colors, customTheme?.colors),
25
+ ...tapestryAliasTokensColorMap[colorScheme],
26
+ },
27
+ }
28
+ }, [colorScheme, customTheme])
29
+
30
+ return memoizedTheme
31
+ }
@@ -0,0 +1,9 @@
1
+ import { useContext } from 'react'
2
+ import { ChatContext } from '../contexts/chat_context'
3
+ import { ChatTheme } from '../utils/theme'
4
+
5
+ export const useTheme = (): ChatTheme => {
6
+ const { theme } = useContext(ChatContext)
7
+
8
+ return theme
9
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,17 @@
1
+ import { Conversations } from './components/conversations'
2
+ import { ChatContext, ChatProvider } from './contexts/chat_context'
3
+ import { useCreateChatTheme } from './hooks'
4
+ import { OAuthToken } from './types'
5
+ import { baseUrlMap, uploadUrlMap } from './utils/session'
6
+ import { TemporaryDefaultColorsType } from './utils/theme'
7
+
8
+ export {
9
+ baseUrlMap,
10
+ ChatContext,
11
+ ChatProvider,
12
+ Conversations,
13
+ OAuthToken,
14
+ TemporaryDefaultColorsType,
15
+ uploadUrlMap,
16
+ useCreateChatTheme,
17
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ export type ConversationRecord = {
2
+ id: string
3
+ type: 'Conversation'
4
+ attributes: {
5
+ title: string
6
+ created_at: string
7
+ updated_at: string
8
+ }
9
+ }
10
+
11
+ export type OAuthToken = {
12
+ token_type: any
13
+ access_token: any
14
+ created_at: number
15
+ expires_in: any
16
+ scope: string
17
+ refresh_token: any
18
+ }
19
+
20
+ // Sets all the properties of of a deeply nested object to optional.
21
+ // Example: `DeepPartial<ChatTheme>`
22
+ export type DeepPartial<T> = {
23
+ [P in keyof T]?: DeepPartial<T[P]>
24
+ }
25
+
26
+ export type RootStackParamList = {
27
+ Settings: undefined
28
+ Conversations: undefined
29
+ }
30
+
31
+ declare global {
32
+ namespace ReactNavigation {
33
+ interface RootParamList extends RootStackParamList {}
34
+ }
35
+ }
@@ -0,0 +1,47 @@
1
+ import { OAuthToken } from '../types'
2
+
3
+ export type ENV = 'production' | 'staging' | 'development'
4
+
5
+ export const baseUrlMap = {
6
+ production: 'https://api.planningcenteronline.com',
7
+ staging: 'https://api-staging.planningcenteronline.com',
8
+ development: 'http://api.pco.test',
9
+ }
10
+
11
+ export const uploadUrlMap = {
12
+ production: 'https://upload.planningcenteronline.com/v2/files',
13
+ staging: 'https://upload-staging.planningcenteronline.com/v2/files',
14
+ development: 'https://upload.planningcenteronline.com/v2/files',
15
+ }
16
+
17
+ type SessionProps = { env?: ENV; token?: OAuthToken } | undefined
18
+
19
+ /**
20
+ * Session class to track the environment and token
21
+ * Not intended to make network requests or handle authentication
22
+ * - returns urls for convenience only
23
+ */
24
+ export class Session {
25
+ env: ENV
26
+ token: OAuthToken | undefined
27
+
28
+ constructor(props?: SessionProps) {
29
+ const { env = 'development', token } = props || {}
30
+ this.env = env
31
+ this.token = token
32
+ }
33
+
34
+ get isAuthenticated() {
35
+ return Boolean(this.token)
36
+ }
37
+
38
+ get baseUrl() {
39
+ return baseUrlMap[this.env]
40
+ }
41
+
42
+ get uploadUrl() {
43
+ return uploadUrlMap[this.env]
44
+ }
45
+ }
46
+
47
+ export const session = new Session({ env: 'development' })
@@ -0,0 +1,73 @@
1
+ import { TextStyle, ViewStyle, ColorSchemeName } from 'react-native'
2
+ import { tokens } from '../vendor/tapestry/tokens'
3
+ import { tapestryAliasTokensColorMap } from '../vendor/tapestry/tapestry_alias_tokens_color_map'
4
+
5
+ export interface ChatTheme extends DefaultTheme {
6
+ colors: DefaultTheme['colors'] &
7
+ (typeof tapestryAliasTokensColorMap.light | typeof tapestryAliasTokensColorMap.dark)
8
+ }
9
+
10
+ /** =============================================
11
+ NOTE: The specific values for `colors` and the `productBadge` are temporary examples that can be replaced once we start building out UI. This line's comment can be removed at that time too.
12
+
13
+ The default theme is intended to support two types of customizations:
14
+ 1. Specific color properties for a chat component (eg. `primaryButtonBackgroundColor`)
15
+ 2. Any styles for a specific component. (Use only one level of nesting.)
16
+ ```
17
+ primaryButton: {
18
+ container: ViewStyle
19
+ text: TextStyle
20
+ }
21
+ ```
22
+ ============================================= */
23
+
24
+ export interface DefaultTheme {
25
+ colors: ChatColors
26
+ temporaryProductBadge: {
27
+ container: ViewStyle
28
+ text: TextStyle
29
+ }
30
+ }
31
+
32
+ export const defaultTheme = (colorScheme: ColorSchemeName): DefaultTheme => {
33
+ const scheme = colorScheme || 'light'
34
+
35
+ return {
36
+ colors: chatThemeColorMap[scheme],
37
+ temporaryProductBadge: {
38
+ container: {
39
+ paddingHorizontal: tokens.spacing1,
40
+ backgroundColor: tapestryAliasTokensColorMap[scheme].fillColorNeutral070,
41
+ borderWidth: tokens.borderSizeDefault,
42
+ },
43
+ text: {
44
+ textAlign: 'center',
45
+ },
46
+ },
47
+ }
48
+ }
49
+
50
+ export type TemporaryDefaultColorsType = Partial<ChatColors>
51
+
52
+ interface ChatColors {
53
+ name: string
54
+ temporaryButtonBackgroundColor: string
55
+ testColor: string
56
+ }
57
+
58
+ const colorsChatLight: ChatColors = {
59
+ name: 'light',
60
+ temporaryButtonBackgroundColor: tokens.colorNeutral95,
61
+ testColor: 'red',
62
+ }
63
+
64
+ const colorsChatDark: ChatColors = {
65
+ name: 'dark',
66
+ temporaryButtonBackgroundColor: tokens.colorNeutral17,
67
+ testColor: 'blue',
68
+ }
69
+
70
+ const chatThemeColorMap = {
71
+ light: colorsChatLight,
72
+ dark: colorsChatDark,
73
+ }
@@ -0,0 +1,82 @@
1
+ import { tokens } from './tokens'
2
+
3
+ // Copied from `@planningcenter/tapestry` package.
4
+ // Defining these tokens locally is a temporary solution until the package supports mobile.
5
+ // Tokens Reference: https://planningcenter.github.io/tapestry/?path=/docs/foundations-design-tokens--docs
6
+
7
+ interface NeutralColors {
8
+ name: string
9
+ fillColorNeutral000: string
10
+ fillColorNeutral010: string
11
+ fillColorNeutral020: string
12
+ fillColorNeutral030: string
13
+ fillColorNeutral040: string
14
+ fillColorNeutral050Base: string
15
+ fillColorNeutral060: string
16
+ fillColorNeutral070: string
17
+ fillColorNeutral080: string
18
+ fillColorNeutral090: string
19
+ fillColorNeutral100Inverted: string
20
+ }
21
+
22
+ const neutralsLight: NeutralColors = {
23
+ name: 'light',
24
+ fillColorNeutral000: tokens.colorNeutral12,
25
+ fillColorNeutral010: tokens.colorNeutral24,
26
+ fillColorNeutral020: tokens.colorNeutral45,
27
+ fillColorNeutral030: tokens.colorNeutral58,
28
+ fillColorNeutral040: tokens.colorNeutral81,
29
+ fillColorNeutral050Base: tokens.colorNeutral88,
30
+ fillColorNeutral060: tokens.colorNeutral93,
31
+ fillColorNeutral070: tokens.colorNeutral95,
32
+ fillColorNeutral080: tokens.colorNeutral97,
33
+ fillColorNeutral090: tokens.colorNeutral98,
34
+ fillColorNeutral100Inverted: tokens.colorNeutral100White,
35
+ }
36
+
37
+ const neutralsDark: NeutralColors = {
38
+ name: 'dark',
39
+ fillColorNeutral000: tokens.colorNeutral98,
40
+ fillColorNeutral010: tokens.colorNeutral88,
41
+ fillColorNeutral020: tokens.colorNeutral68,
42
+ fillColorNeutral030: tokens.colorNeutral50,
43
+ fillColorNeutral040: tokens.colorNeutral32,
44
+ fillColorNeutral050Base: tokens.colorNeutral24,
45
+ fillColorNeutral060: tokens.colorNeutral19,
46
+ fillColorNeutral070: tokens.colorNeutral17,
47
+ fillColorNeutral080: tokens.colorNeutral15,
48
+ fillColorNeutral090: tokens.colorNeutral12,
49
+ fillColorNeutral100Inverted: tokens.colorNeutral7,
50
+ }
51
+
52
+ interface SemanticAliasesColors {
53
+ name: string
54
+ iconColorDefaultPrimary: string
55
+ iconColorDefaultSecondary: string
56
+ iconColorDefaultDim: string
57
+ iconColorDefaultDisabled: string
58
+ iconColorDefaultInverted: string
59
+ }
60
+
61
+ const semanticAliasesLight: SemanticAliasesColors = {
62
+ name: 'light',
63
+ iconColorDefaultPrimary: neutralsLight.fillColorNeutral010,
64
+ iconColorDefaultSecondary: neutralsLight.fillColorNeutral020,
65
+ iconColorDefaultDim: neutralsLight.fillColorNeutral030,
66
+ iconColorDefaultDisabled: neutralsLight.fillColorNeutral040,
67
+ iconColorDefaultInverted: neutralsLight.fillColorNeutral100Inverted,
68
+ }
69
+
70
+ const semanticAliasesDark: SemanticAliasesColors = {
71
+ name: 'dark',
72
+ iconColorDefaultPrimary: neutralsDark.fillColorNeutral010,
73
+ iconColorDefaultSecondary: neutralsDark.fillColorNeutral020,
74
+ iconColorDefaultDim: neutralsDark.fillColorNeutral030,
75
+ iconColorDefaultDisabled: neutralsDark.fillColorNeutral040,
76
+ iconColorDefaultInverted: neutralsDark.fillColorNeutral100Inverted,
77
+ }
78
+
79
+ export const tapestryAliasTokensColorMap = {
80
+ light: { ...neutralsLight, ...semanticAliasesLight },
81
+ dark: { ...neutralsDark, ...semanticAliasesDark },
82
+ }
@@ -0,0 +1,97 @@
1
+ // Copied from `@planningcenter/tapestry` package.
2
+ // Defining these tokens locally is a temporary solution until the package supports mobile.
3
+ // Tokens Reference: https://planningcenter.github.io/tapestry/?path=/docs/foundations-design-tokens--docs
4
+
5
+ interface ColorPrimitves {
6
+ colorNeutral7: string
7
+ colorNeutral12: string
8
+ colorNeutral15: string
9
+ colorNeutral17: string
10
+ colorNeutral19: string
11
+ colorNeutral24: string
12
+ colorNeutral32: string
13
+ colorNeutral45: string
14
+ colorNeutral50: string
15
+ colorNeutral58: string
16
+ colorNeutral68: string
17
+ colorNeutral81: string
18
+ colorNeutral88: string
19
+ colorNeutral93: string
20
+ colorNeutral95: string
21
+ colorNeutral97: string
22
+ colorNeutral98: string
23
+ colorNeutral100White: string
24
+ }
25
+
26
+ const colorPrimitives: ColorPrimitves = {
27
+ colorNeutral7: 'hsl(0, 0%, 7%)',
28
+ colorNeutral12: 'hsl(0, 0%, 12%)',
29
+ colorNeutral15: 'hsl(0, 0%, 15%)',
30
+ colorNeutral17: 'hsl(0, 0%, 17%)',
31
+ colorNeutral19: 'hsl(0, 0%, 19%)',
32
+ colorNeutral24: 'hsl(0, 0%, 24%)',
33
+ colorNeutral32: 'hsl(0, 0%, 32%)',
34
+ colorNeutral45: 'hsl(0, 0%, 45%)',
35
+ colorNeutral50: 'hsl(0, 0%, 50%)',
36
+ colorNeutral58: 'hsl(0, 0%, 58%)',
37
+ colorNeutral68: 'hsl(0, 0%, 68%)',
38
+ colorNeutral81: 'hsl(0, 0%, 81%)',
39
+ colorNeutral88: 'hsl(0, 0%, 88%)',
40
+ colorNeutral93: 'hsl(0, 0%, 93%)',
41
+ colorNeutral95: 'hsl(0, 0%, 95%)',
42
+ colorNeutral97: 'hsl(0, 0%, 97%)',
43
+ colorNeutral98: 'hsl(0, 0%, 98%)',
44
+ colorNeutral100White: 'hsl(0, 0%, 100%)',
45
+ }
46
+
47
+ interface NumericPrimitives {
48
+ spacingFourth: number
49
+ spacingHalf: number
50
+ spacing1: number
51
+ spacing2: number
52
+ spacing3: number
53
+ spacing4: number
54
+ spacing5: number
55
+ spacing6: number
56
+ spacing7: number
57
+ borderRadiusSm: number
58
+ borderRadiusMd: number
59
+ borderRadiusLg: number
60
+ borderRadiusXl: number
61
+ borderRadiusRound: number
62
+ borderSizeDefault: number
63
+ borderSizeThick: number
64
+ }
65
+
66
+ const numericPrimtives: NumericPrimitives = {
67
+ spacingFourth: 2,
68
+ spacingHalf: 4,
69
+ spacing1: 8,
70
+ spacing2: 16,
71
+ spacing3: 24,
72
+ spacing4: 32,
73
+ spacing5: 40,
74
+ spacing6: 48,
75
+ spacing7: 56,
76
+ borderRadiusSm: 2,
77
+ borderRadiusMd: 4,
78
+ borderRadiusLg: 8,
79
+ borderRadiusXl: 16,
80
+ borderRadiusRound: 56,
81
+ borderSizeDefault: 1,
82
+ borderSizeThick: 2,
83
+ }
84
+
85
+ interface NumericAliases {
86
+ borderRadiusDefault: number
87
+ }
88
+
89
+ const numericAliases: NumericAliases = {
90
+ borderRadiusDefault: numericPrimtives.borderRadiusMd,
91
+ }
92
+
93
+ export const tokens = {
94
+ ...colorPrimitives,
95
+ ...numericPrimtives,
96
+ ...numericAliases,
97
+ }