@umituz/react-native-settings 4.20.52 → 4.20.54

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 (24) hide show
  1. package/package.json +1 -1
  2. package/src/domains/about/presentation/components/AboutSettingItem.tsx +39 -86
  3. package/src/domains/about/presentation/hooks/useAboutInfo.ts +39 -101
  4. package/src/domains/about/presentation/hooks/useAboutInfo.types.ts +32 -0
  5. package/src/domains/about/utils/AppInfoFactory.ts +19 -0
  6. package/src/domains/about/utils/index.ts +2 -0
  7. package/src/domains/legal/index.ts +1 -0
  8. package/src/domains/legal/presentation/screens/LegalContentScreen.tsx +140 -0
  9. package/src/domains/legal/presentation/screens/PrivacyPolicyScreen.tsx +17 -155
  10. package/src/domains/legal/presentation/screens/TermsOfServiceScreen.tsx +17 -155
  11. package/src/presentation/navigation/SettingsStackNavigator.tsx +50 -129
  12. package/src/presentation/navigation/components/wrappers/AboutScreenWrapper.tsx +13 -0
  13. package/src/presentation/navigation/components/wrappers/LegalScreenWrapper.tsx +50 -0
  14. package/src/presentation/navigation/components/wrappers/SettingsScreenWrapper.tsx +32 -0
  15. package/src/presentation/navigation/components/wrappers/index.ts +9 -0
  16. package/src/presentation/navigation/utils/index.ts +5 -0
  17. package/src/presentation/navigation/utils/navigationScreenOptions.ts +56 -0
  18. package/src/presentation/navigation/utils/navigationTranslations.ts +46 -0
  19. package/src/presentation/screens/types/BaseTypes.ts +12 -0
  20. package/src/presentation/screens/types/ContentConfig.ts +82 -0
  21. package/src/presentation/screens/types/SettingsConfig.ts +6 -4
  22. package/src/presentation/screens/types/UserFeatureConfig.ts +137 -0
  23. package/src/presentation/screens/types/index.ts +10 -8
  24. package/src/presentation/screens/types/FeatureConfig.ts +0 -263
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-settings",
3
- "version": "4.20.52",
3
+ "version": "4.20.54",
4
4
  "description": "Complete settings hub for React Native apps - consolidated package with settings, about, legal, appearance, feedback, FAQs, and rating",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -4,7 +4,7 @@
4
4
  * Fully configurable and generic
5
5
  * Optimized for performance and memory safety
6
6
  */
7
- import React, { useMemo, useCallback } from 'react';
7
+ import React, { memo, useCallback } from 'react';
8
8
  import {
9
9
  TouchableOpacity,
10
10
  View,
@@ -13,6 +13,7 @@ import {
13
13
  ViewStyle,
14
14
  TextStyle,
15
15
  } from 'react-native';
16
+ import { useAppDesignTokens } from '@umituz/react-native-design-system';
16
17
 
17
18
  export interface AboutSettingItemProps {
18
19
  /** Icon component (any React component) */
@@ -45,9 +46,7 @@ export interface AboutSettingItemProps {
45
46
  chevronColor?: string;
46
47
  }
47
48
 
48
- import { useAppDesignTokens } from '@umituz/react-native-design-system';
49
-
50
- export const AboutSettingItem: React.FC<AboutSettingItemProps> = ({
49
+ export const AboutSettingItem: React.FC<AboutSettingItemProps> = memo(({
51
50
  icon,
52
51
  title,
53
52
  description,
@@ -67,107 +66,61 @@ export const AboutSettingItem: React.FC<AboutSettingItemProps> = ({
67
66
  const styles = getStyles(tokens);
68
67
  const colors = tokens.colors;
69
68
 
70
- // Memoize container type to prevent unnecessary re-renders
71
- const Container = useMemo(() => {
72
- return onPress ? TouchableOpacity : View;
73
- }, [onPress]) as React.ComponentType<React.ComponentProps<typeof TouchableOpacity | typeof View>>;
74
-
75
- // Memoize container styles
76
- const containerStyles = useMemo(() => {
77
- return [
78
- styles.container,
79
- { backgroundColor: colors.surface },
80
- disabled && styles.disabled,
81
- containerStyle
82
- ];
83
- }, [disabled, containerStyle, colors.surface, styles]);
69
+ const Container = onPress ? TouchableOpacity : View;
84
70
 
85
- // Memoize icon container styles
86
- const iconContainerStyles = useMemo(() => {
87
- return [
88
- styles.iconContainer,
89
- iconContainerStyle
90
- ];
91
- }, [iconContainerStyle, styles]);
92
-
93
- // Memoize chevron styles
94
- const chevronStyles = useMemo(() => {
95
- return [
96
- styles.chevron,
97
- { color: chevronColor || colors.textSecondary }
98
- ];
99
- }, [chevronColor, colors.textSecondary, styles]);
100
-
101
- // Memoize press handler to prevent unnecessary re-renders
102
71
  const handlePress = useCallback(() => {
103
72
  if (onPress && !disabled) {
104
73
  onPress();
105
74
  }
106
75
  }, [onPress, disabled]);
107
76
 
108
- // Memoize icon rendering
109
- const renderIcon = useMemo(() => {
110
- if (!icon) {
111
- return null;
112
- }
77
+ const containerStyles = [
78
+ styles.container,
79
+ { backgroundColor: colors.surface },
80
+ disabled && styles.disabled,
81
+ containerStyle,
82
+ ];
113
83
 
114
- return (
115
- <View style={iconContainerStyles}>
116
- {icon}
117
- </View>
118
- );
119
- }, [icon, iconContainerStyles]);
84
+ const iconContainerStyles = [
85
+ styles.iconContainer,
86
+ iconContainerStyle,
87
+ ];
88
+
89
+ const chevronStyles = [
90
+ styles.chevron,
91
+ { color: chevronColor || colors.textSecondary },
92
+ ];
93
+
94
+ return (
95
+ <Container
96
+ style={containerStyles}
97
+ onPress={handlePress}
98
+ disabled={disabled}
99
+ testID={testID}
100
+ >
101
+ {icon && <View style={iconContainerStyles}>{icon}</View>}
120
102
 
121
- // Memoize content rendering
122
- const renderContent = useMemo(() => {
123
- return (
124
103
  <View style={styles.content}>
125
- <Text style={[styles.title, { color: colors.textPrimary }, titleStyle]}>{title}</Text>
104
+ <Text style={[styles.title, { color: colors.textPrimary }, titleStyle]}>
105
+ {title}
106
+ </Text>
126
107
  {description && (
127
108
  <Text style={[styles.description, { color: colors.textSecondary }, descriptionStyle]}>
128
109
  {description}
129
110
  </Text>
130
111
  )}
131
112
  </View>
132
- );
133
- }, [title, description, titleStyle, descriptionStyle, colors.textPrimary, colors.textSecondary, styles]);
134
-
135
- // Memoize value rendering
136
- const renderValue = useMemo(() => {
137
- if (!value) {
138
- return null;
139
- }
140
-
141
- return (
142
- <Text style={[styles.value, { color: colors.textSecondary }, valueStyle]}>{value}</Text>
143
- );
144
- }, [value, valueStyle, colors.textSecondary, styles]);
145
-
146
- // Memoize chevron rendering
147
- const renderChevron = useMemo(() => {
148
- if (!showChevron) {
149
- return null;
150
- }
151
113
 
152
- return (
153
- <Text style={chevronStyles}>›</Text>
154
- );
155
- }, [showChevron, chevronStyles]);
114
+ {value && (
115
+ <Text style={[styles.value, { color: colors.textSecondary }, valueStyle]}>
116
+ {value}
117
+ </Text>
118
+ )}
156
119
 
157
- return (
158
- <Container
159
- style={containerStyles}
160
- onPress={handlePress}
161
- disabled={disabled}
162
- testID={testID}
163
- >
164
- {renderIcon}
165
- {renderContent}
166
- {renderValue}
167
- {renderChevron}
120
+ {showChevron && <Text style={chevronStyles}>›</Text>}
168
121
  </Container>
169
122
  );
170
- };
123
+ });
171
124
 
172
125
  const getStyles = (tokens: any) => StyleSheet.create({
173
126
  container: {
@@ -206,4 +159,4 @@ const getStyles = (tokens: any) => StyleSheet.create({
206
159
  fontSize: 20,
207
160
  fontWeight: '300',
208
161
  },
209
- });
162
+ });
@@ -3,9 +3,10 @@
3
3
  * Provides reactive state management for About data
4
4
  * Optimized for performance and memory safety
5
5
  */
6
- import { useState, useEffect, useCallback, useRef } from 'react';
6
+ import { useState, useCallback, useRef, useEffect } from 'react';
7
7
  import { AppInfo, AboutConfig } from '../../domain/entities/AppInfo';
8
8
  import { AboutRepository } from '../../infrastructure/repositories/AboutRepository';
9
+ import { createDefaultAppInfo } from '../../utils/AppInfoFactory';
9
10
 
10
11
  export interface UseAboutInfoOptions {
11
12
  /** Initial configuration */
@@ -42,161 +43,114 @@ export const useAboutInfo = (
42
43
  const [loading, setLoading] = useState(false);
43
44
  const [error, setError] = useState<string | null>(null);
44
45
 
45
- // Prevent infinite loops and memory leaks
46
46
  const isInitializedRef = useRef(false);
47
47
  const isMountedRef = useRef(true);
48
48
 
49
+ const setErrorIfMounted = useCallback((err: string | null) => {
50
+ if (isMountedRef.current) {
51
+ setError(err);
52
+ }
53
+ }, []);
54
+
55
+ const setLoadingIfMounted = useCallback((value: boolean) => {
56
+ if (isMountedRef.current) {
57
+ setLoading(value);
58
+ }
59
+ }, []);
60
+
49
61
  const initialize = useCallback(async (config: AboutConfig, force = false) => {
50
- // Prevent multiple initializations unless forced
51
62
  if (isInitializedRef.current && !force) {
52
63
  return;
53
64
  }
54
65
 
55
- // Check if component is still mounted
56
66
  if (!isMountedRef.current) {
57
67
  return;
58
68
  }
59
69
 
60
- setLoading(true);
61
- setError(null);
70
+ setLoadingIfMounted(true);
71
+ setErrorIfMounted(null);
62
72
 
63
73
  try {
64
- const defaultAppInfo: AppInfo = {
65
- name: config.appInfo?.name || '',
66
- version: config.appInfo?.version || '1.0.0',
67
- description: config.appInfo?.description,
68
- developer: config.appInfo?.developer,
69
- contactEmail: config.appInfo?.contactEmail,
70
- websiteUrl: config.appInfo?.websiteUrl,
71
- websiteDisplay: config.appInfo?.websiteDisplay,
72
- moreAppsUrl: config.appInfo?.moreAppsUrl,
73
- };
74
-
74
+ const defaultAppInfo = createDefaultAppInfo(config);
75
75
  await repository.saveAppInfo(defaultAppInfo);
76
76
 
77
- // Only update state if component is still mounted
78
77
  if (isMountedRef.current) {
79
78
  setAppInfo(defaultAppInfo);
80
79
  isInitializedRef.current = true;
81
80
  }
82
81
  } catch (err) {
83
- if (!isMountedRef.current) {
84
- return;
85
- }
86
-
87
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
88
- setError(errorMessage);
82
+ setErrorIfMounted(err instanceof Error ? err.message : 'Unknown error');
89
83
  } finally {
90
- // Only update loading state if component is still mounted
91
- if (isMountedRef.current) {
92
- setLoading(false);
93
- }
84
+ setLoadingIfMounted(false);
94
85
  }
95
- }, [repository]);
86
+ }, [repository, setErrorIfMounted, setLoadingIfMounted]);
96
87
 
97
88
  const update = useCallback(async (config: AboutConfig) => {
98
89
  if (!isMountedRef.current) {
99
90
  return;
100
91
  }
101
92
 
102
- setLoading(true);
103
- setError(null);
93
+ setLoadingIfMounted(true);
94
+ setErrorIfMounted(null);
104
95
 
105
96
  try {
106
- const updatedAppInfo: AppInfo = {
107
- name: config.appInfo?.name || '',
108
- version: config.appInfo?.version || '1.0.0',
109
- description: config.appInfo?.description,
110
- developer: config.appInfo?.developer,
111
- contactEmail: config.appInfo?.contactEmail,
112
- websiteUrl: config.appInfo?.websiteUrl,
113
- websiteDisplay: config.appInfo?.websiteDisplay,
114
- moreAppsUrl: config.appInfo?.moreAppsUrl,
115
- };
116
-
97
+ const updatedAppInfo = createDefaultAppInfo(config);
117
98
  await repository.saveAppInfo(updatedAppInfo);
118
99
 
119
- // Only update state if component is still mounted
120
100
  if (isMountedRef.current) {
121
101
  setAppInfo(updatedAppInfo);
122
102
  }
123
103
  } catch (err) {
124
- if (!isMountedRef.current) {
125
- return;
126
- }
127
-
128
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
129
- setError(errorMessage);
104
+ setErrorIfMounted(err instanceof Error ? err.message : 'Unknown error');
130
105
  } finally {
131
- // Only update loading state if component is still mounted
132
- if (isMountedRef.current) {
133
- setLoading(false);
134
- }
106
+ setLoadingIfMounted(false);
135
107
  }
136
- }, [repository]);
108
+ }, [repository, setErrorIfMounted, setLoadingIfMounted]);
137
109
 
138
110
  const updateAppInfo = useCallback(async (updates: Partial<AppInfo>) => {
139
111
  if (!appInfo || !isMountedRef.current) {
140
112
  if (isMountedRef.current) {
141
- setError('App info not initialized');
113
+ setErrorIfMounted('App info not initialized');
142
114
  }
143
115
  return;
144
116
  }
145
117
 
146
- setLoading(true);
147
- setError(null);
118
+ setLoadingIfMounted(true);
119
+ setErrorIfMounted(null);
148
120
 
149
121
  try {
150
122
  const updatedInfo = await repository.updateAppInfo(updates);
151
123
 
152
- // Only update state if component is still mounted
153
124
  if (isMountedRef.current) {
154
125
  setAppInfo(updatedInfo);
155
126
  }
156
127
  } catch (err) {
157
- if (!isMountedRef.current) {
158
- return;
159
- }
160
-
161
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
162
- setError(errorMessage);
128
+ setErrorIfMounted(err instanceof Error ? err.message : 'Unknown error');
163
129
  } finally {
164
- // Only update loading state if component is still mounted
165
- if (isMountedRef.current) {
166
- setLoading(false);
167
- }
130
+ setLoadingIfMounted(false);
168
131
  }
169
- }, [repository, appInfo]);
132
+ }, [repository, appInfo, setErrorIfMounted, setLoadingIfMounted]);
170
133
 
171
134
  const refresh = useCallback(async () => {
172
135
  if (!isMountedRef.current || !appInfo) {
173
136
  return;
174
137
  }
175
138
 
176
- setLoading(true);
177
- setError(null);
139
+ setLoadingIfMounted(true);
140
+ setErrorIfMounted(null);
178
141
 
179
142
  try {
180
143
  const refreshedInfo = await repository.getAppInfo();
181
144
 
182
- // Only update state if component is still mounted
183
145
  if (isMountedRef.current) {
184
146
  setAppInfo(refreshedInfo);
185
147
  }
186
148
  } catch (err) {
187
- if (!isMountedRef.current) {
188
- return;
189
- }
190
-
191
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
192
- setError(errorMessage);
149
+ setErrorIfMounted(err instanceof Error ? err.message : 'Unknown error');
193
150
  } finally {
194
- // Only update loading state if component is still mounted
195
- if (isMountedRef.current) {
196
- setLoading(false);
197
- }
151
+ setLoadingIfMounted(false);
198
152
  }
199
- }, [repository, appInfo]);
153
+ }, [repository, appInfo, setErrorIfMounted, setLoadingIfMounted]);
200
154
 
201
155
  const reset = useCallback(() => {
202
156
  if (!isMountedRef.current) {
@@ -209,40 +163,24 @@ export const useAboutInfo = (
209
163
  isInitializedRef.current = false;
210
164
  }, []);
211
165
 
212
- // Cleanup on unmount to prevent memory leaks
213
166
  useEffect(() => {
214
167
  return () => {
215
168
  isMountedRef.current = false;
216
169
 
217
- // Cleanup repository if it has destroy method
218
170
  if (repository && typeof repository.destroy === 'function') {
219
171
  repository.destroy();
220
172
  }
221
173
  };
222
174
  }, [repository]);
223
175
 
224
-
225
-
226
- // Set initial config when provided (if autoInit is not explicitly false)
227
176
  useEffect(() => {
228
177
  if (initialConfig && autoInit !== false && isMountedRef.current && !isInitializedRef.current) {
229
- const defaultAppInfo: AppInfo = {
230
- name: initialConfig.appInfo?.name || '',
231
- version: initialConfig.appInfo?.version || '1.0.0',
232
- description: initialConfig.appInfo?.description,
233
- developer: initialConfig.appInfo?.developer,
234
- contactEmail: initialConfig.appInfo?.contactEmail,
235
- websiteUrl: initialConfig.appInfo?.websiteUrl,
236
- websiteDisplay: initialConfig.appInfo?.websiteDisplay,
237
- moreAppsUrl: initialConfig.appInfo?.moreAppsUrl,
238
- };
239
-
178
+ const defaultAppInfo = createDefaultAppInfo(initialConfig);
240
179
  setAppInfo(defaultAppInfo);
241
180
  isInitializedRef.current = true;
242
181
  }
243
182
  }, [initialConfig, autoInit]);
244
183
 
245
- // Auto-initialize with dependency optimization
246
184
  useEffect(() => {
247
185
  if (autoInit === true && initialConfig && isMountedRef.current) {
248
186
  initialize(initialConfig, true);
@@ -259,4 +197,4 @@ export const useAboutInfo = (
259
197
  refresh,
260
198
  reset,
261
199
  };
262
- };
200
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * useAboutInfo Hook Types
3
+ * Type definitions for about info hook
4
+ */
5
+
6
+ import type { AppInfo, AboutConfig } from '../../domain/entities/AppInfo';
7
+
8
+ export interface UseAboutInfoOptions {
9
+ /** Initial configuration */
10
+ initialConfig?: AboutConfig;
11
+ /** Auto-initialize on mount */
12
+ autoInit?: boolean;
13
+ }
14
+
15
+ export interface UseAboutInfoReturn {
16
+ /** Current app info */
17
+ appInfo: AppInfo | null;
18
+ /** Loading state */
19
+ loading: boolean;
20
+ /** Error state */
21
+ error: string | null;
22
+ /** Initialize with config */
23
+ initialize: (config: AboutConfig) => Promise<void>;
24
+ /** Update with new config */
25
+ update: (config: AboutConfig) => Promise<void>;
26
+ /** Update app info */
27
+ updateAppInfo: (updates: Partial<AppInfo>) => Promise<void>;
28
+ /** Refresh current app info */
29
+ refresh: () => Promise<void>;
30
+ /** Reset to initial state */
31
+ reset: () => void;
32
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * AppInfo Factory Utility
3
+ * Creates AppInfo objects from AboutConfig
4
+ */
5
+ import { AppInfo, AboutConfig } from '../domain/entities/AppInfo';
6
+
7
+ /**
8
+ * Creates a default AppInfo object from AboutConfig
9
+ */
10
+ export const createDefaultAppInfo = (config: AboutConfig): AppInfo => ({
11
+ name: config.appInfo?.name || '',
12
+ version: config.appInfo?.version || '1.0.0',
13
+ description: config.appInfo?.description,
14
+ developer: config.appInfo?.developer,
15
+ contactEmail: config.appInfo?.contactEmail,
16
+ websiteUrl: config.appInfo?.websiteUrl,
17
+ websiteDisplay: config.appInfo?.websiteDisplay,
18
+ moreAppsUrl: config.appInfo?.moreAppsUrl,
19
+ });
@@ -3,6 +3,8 @@
3
3
  * General purpose utilities for all applications
4
4
  */
5
5
 
6
+ export { createDefaultAppInfo } from './AppInfoFactory';
7
+
6
8
  /**
7
9
  * Create default configuration with overrides
8
10
  */
@@ -6,3 +6,4 @@
6
6
  export * from './presentation/screens/LegalScreen';
7
7
  export * from './presentation/screens/PrivacyPolicyScreen';
8
8
  export * from './presentation/screens/TermsOfServiceScreen';
9
+ export { LegalContentScreen } from './presentation/screens/LegalContentScreen';
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Base Legal Content Screen Component
3
+ * Shared logic for legal document screens
4
+ */
5
+ import React from "react";
6
+ import { View, ScrollView, StyleSheet } from "react-native";
7
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system";
9
+ import { AtomicText, AtomicButton } from "@umituz/react-native-design-system";
10
+ import { UrlHandlerService } from "../../domain/services/UrlHandlerService";
11
+ import { ContentValidationService } from "../../domain/services/ContentValidationService";
12
+ import { StyleCacheService } from "../../domain/services/StyleCacheService";
13
+
14
+ export interface LegalContentScreenProps {
15
+ content?: string;
16
+ url?: string;
17
+ title: string;
18
+ viewOnlineText?: string;
19
+ openText?: string;
20
+ onUrlPress?: () => void;
21
+ testID?: string;
22
+ styleCacheKey: string;
23
+ createStyles: (tokens: any) => ReturnType<typeof StyleSheet.create>;
24
+ }
25
+
26
+ export const LegalContentScreen: React.FC<LegalContentScreenProps> = React.memo(({
27
+ content,
28
+ url,
29
+ title,
30
+ viewOnlineText,
31
+ openText,
32
+ onUrlPress,
33
+ testID,
34
+ styleCacheKey,
35
+ createStyles,
36
+ }) => {
37
+ const tokens = useAppDesignTokens();
38
+ const insets = useSafeAreaInsets();
39
+
40
+ React.useEffect(() => {
41
+ ContentValidationService.validateScreenContent(
42
+ content,
43
+ url,
44
+ title,
45
+ viewOnlineText,
46
+ openText,
47
+ styleCacheKey
48
+ );
49
+ }, [content, url, title, viewOnlineText, openText, styleCacheKey]);
50
+
51
+ const styles = React.useMemo(() => {
52
+ const cacheKey = StyleCacheService.createTokenCacheKey(tokens);
53
+ return StyleCacheService.getCachedStyles(
54
+ styleCacheKey,
55
+ cacheKey,
56
+ () => createStyles(tokens)
57
+ );
58
+ }, [tokens, styleCacheKey, createStyles]);
59
+
60
+ const handleUrlPress = React.useCallback(async () => {
61
+ if (onUrlPress) {
62
+ onUrlPress();
63
+ } else if (url) {
64
+ try {
65
+ await UrlHandlerService.openUrl(url);
66
+ } catch {
67
+ // Silent error handling
68
+ }
69
+ }
70
+ }, [onUrlPress, url]);
71
+
72
+ const containerStyle = React.useMemo(() => [
73
+ styles.container,
74
+ {
75
+ backgroundColor: tokens.colors.backgroundPrimary,
76
+ paddingTop: insets.top,
77
+ },
78
+ ], [styles.container, tokens.colors.backgroundPrimary, insets.top]);
79
+
80
+ const showContent = React.useMemo(() => !!(content), [content]);
81
+ const showUrlSection = React.useMemo(() =>
82
+ ContentValidationService.shouldShowUrlSection(url, onUrlPress),
83
+ [url, onUrlPress]
84
+ );
85
+
86
+ const contentSection = React.useMemo(() => {
87
+ if (showContent) {
88
+ return (
89
+ <AtomicText type="bodyMedium" color="onSurface" style={styles.text}>
90
+ {content}
91
+ </AtomicText>
92
+ );
93
+ }
94
+
95
+ if (showUrlSection) {
96
+ return (
97
+ <View style={styles.urlContainer}>
98
+ <AtomicText
99
+ type="bodyMedium"
100
+ color="secondary"
101
+ style={styles.urlText}
102
+ >
103
+ {viewOnlineText}
104
+ </AtomicText>
105
+ <AtomicButton
106
+ variant="primary"
107
+ onPress={handleUrlPress}
108
+ fullWidth
109
+ style={styles.urlButton}
110
+ >
111
+ {openText}
112
+ </AtomicButton>
113
+ </View>
114
+ );
115
+ }
116
+
117
+ return null;
118
+ }, [showContent, showUrlSection, styles, content, viewOnlineText, openText, handleUrlPress]);
119
+
120
+ return (
121
+ <View style={containerStyle} testID={testID}>
122
+ <ScrollView
123
+ contentContainerStyle={styles.scrollContent}
124
+ showsVerticalScrollIndicator={false}
125
+ >
126
+ <View style={styles.content}>
127
+ <AtomicText
128
+ type="headlineLarge"
129
+ color="primary"
130
+ style={styles.title}
131
+ >
132
+ {title}
133
+ </AtomicText>
134
+
135
+ {contentSection}
136
+ </View>
137
+ </ScrollView>
138
+ </View>
139
+ );
140
+ });