@opndev/react-native-events 0.0.10

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,196 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
+
5
+ import React, {
6
+ useEffect,
7
+ useState,
8
+ } from 'react';
9
+ import {
10
+ ActivityIndicator,
11
+ Pressable,
12
+ RefreshControl,
13
+ ScrollView,
14
+ StyleSheet,
15
+ Text,
16
+ } from 'react-native';
17
+
18
+
19
+ import { fetchNewsJson } from '../actions/news';
20
+ import Markdown from 'react-native-markdown-display';
21
+
22
+ const defaultStyle = StyleSheet.create({
23
+ container: {
24
+ flex: 1,
25
+ },
26
+ content: {
27
+ padding: 16,
28
+ gap: 12,
29
+ },
30
+ item: {
31
+ padding: 16,
32
+ borderRadius: 12,
33
+ gap: 8,
34
+ },
35
+ });
36
+
37
+ /**
38
+ * NewsListScreen
39
+ *
40
+ * Fetches and renders a vertically scrollable list of news items.
41
+ *
42
+ * @param {object} props
43
+ * @param {string} props.uri
44
+ * @param {string} [props.bearerToken]
45
+ * @param {React.ComponentType} [props.TextComponent]
46
+ * @param {Function} [props.onPressItem]
47
+ * @param {string} [props.backgroundColor]
48
+ * @param {string} [props.cardBackgroundColor]
49
+ * @param {object} [props.containerStyle]
50
+ * @param {object} [props.contentStyle]
51
+ * @param {object} [props.itemStyle]
52
+ * @param {object} [props.titleStyle]
53
+ * @param {object} [props.summaryStyle]
54
+ * @param {object} [props.refreshButtonStyle]
55
+ * @param {object} [props.refreshTextStyle]
56
+ * @param {object} [props.errorTextStyle]
57
+ * @param {string} [props.refreshLabel]
58
+ * @param {string} [props.emptyLabel]
59
+ * @param {string} [props.errorLabel]
60
+ * @param {boolean} [props.showRefresh]
61
+ *
62
+ * @returns {JSX.Element}
63
+ */
64
+ export default function NewsListScreen({
65
+ uri,
66
+ bearerToken,
67
+ TextComponent = Text,
68
+ MarkdownComponent = Markdown,
69
+ onPressItem,
70
+
71
+ backgroundColor,
72
+ cardBackgroundColor,
73
+
74
+ containerStyle,
75
+ contentStyle,
76
+ itemStyle,
77
+ titleStyle,
78
+ summaryStyle,
79
+ refreshButtonStyle,
80
+ refreshTextStyle,
81
+ errorTextStyle,
82
+
83
+ refreshLabel = 'Refresh',
84
+ emptyLabel = 'No news items.',
85
+ errorLabel = 'Unable to load news.',
86
+ showRefresh = true,
87
+ }) {
88
+ const [items, setItems] = useState([]);
89
+ const [loading, setLoading] = useState(true);
90
+ const [refreshing, setRefreshing] = useState(false);
91
+ const [error, setError] = useState(null);
92
+
93
+ useEffect(() => {
94
+ load();
95
+ }, [uri, bearerToken]);
96
+
97
+ /**
98
+ * Loads news items from the configured URI.
99
+ *
100
+ * @param {boolean} [force]
101
+ *
102
+ * @returns {Promise<void>}
103
+ */
104
+ async function load(force = false) {
105
+ setError(null);
106
+
107
+ if (force) {
108
+ setRefreshing(true);
109
+ }
110
+ else {
111
+ setLoading(true);
112
+ }
113
+
114
+ try {
115
+ const data = await fetchNewsJson({
116
+ uri,
117
+ bearerToken,
118
+ force,
119
+ });
120
+
121
+ setItems(data.items);
122
+ }
123
+ catch (e) {
124
+ setError(e.message || errorLabel);
125
+ }
126
+
127
+ if (force) {
128
+ setRefreshing(false);
129
+ }
130
+ else {
131
+ setLoading(false);
132
+ }
133
+ }
134
+
135
+ return (
136
+ <ScrollView
137
+ style={[
138
+ defaultStyle.container,
139
+ backgroundColor ? { backgroundColor } : null,
140
+ containerStyle,
141
+ ]}
142
+ contentContainerStyle={[
143
+ defaultStyle.content,
144
+ contentStyle,
145
+ ]}
146
+ refreshControl={
147
+ <RefreshControl
148
+ refreshing={refreshing}
149
+ onRefresh={() => load(true)}
150
+ />
151
+ }
152
+ >
153
+ {loading ? (
154
+ <ActivityIndicator />
155
+ ) : null}
156
+
157
+ {!loading && error ? (
158
+ <TextComponent style={errorTextStyle}>
159
+ {error}
160
+ </TextComponent>
161
+ ) : null}
162
+
163
+ {!loading && !error && !items.length ? (
164
+ <TextComponent style={errorTextStyle}>
165
+ {emptyLabel}
166
+ </TextComponent>
167
+ ) : null}
168
+
169
+ {!loading && !error && items.map((item) => {
170
+ return (
171
+ <Pressable
172
+ key={item.id}
173
+ style={[
174
+ defaultStyle.item,
175
+ cardBackgroundColor
176
+ ? { backgroundColor: cardBackgroundColor }
177
+ : null,
178
+ itemStyle,
179
+ ]}
180
+ onPress={() => onPressItem?.(item)}
181
+ >
182
+ <TextComponent style={titleStyle}>
183
+ {item.title}
184
+ </TextComponent>
185
+
186
+ {item.summary ? (
187
+ <MarkdownComponent style={summaryStyle}>
188
+ {item.summary}
189
+ </MarkdownComponent>
190
+ ) : null}
191
+ </Pressable>
192
+ );
193
+ })}
194
+ </ScrollView>
195
+ );
196
+ }
@@ -0,0 +1,56 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
+
5
+ import React, { useMemo } from 'react';
6
+
7
+ import QRCodeForm from '../components/qr-code-form';
8
+ import QRCodeAction from '../actions/qrcode';
9
+
10
+ /**
11
+ * QRCodeScreen
12
+ *
13
+ * QR code feature content.
14
+ *
15
+ * @param {object} props
16
+ * @param {React.ComponentType} props.TextComponent
17
+ * @param {{url: string, method?: string, headers?: object}} props.endpoint
18
+ * @param {Array<object>} props.fields
19
+ * @param {string} [props.storageKey]
20
+ * @param {(values: object) => any} [props.buildPayload]
21
+ * @param {(response: any) => string} [props.extractToken]
22
+ *
23
+ * @returns {JSX.Element}
24
+ */
25
+ export default function QRCodeScreen({
26
+ TextComponent,
27
+ endpoint,
28
+ fields,
29
+ storageKey,
30
+ buildPayload,
31
+ extractToken,
32
+ ...rest
33
+ }) {
34
+ const action = useMemo(() => {
35
+ return new QRCodeAction({
36
+ endpoint,
37
+ storageKey,
38
+ buildPayload,
39
+ extractToken,
40
+ });
41
+ }, [
42
+ endpoint,
43
+ storageKey,
44
+ buildPayload,
45
+ extractToken,
46
+ ]);
47
+
48
+ return (
49
+ <QRCodeForm
50
+ action={action}
51
+ fields={fields}
52
+ TextComponent={TextComponent}
53
+ {...rest}
54
+ />
55
+ );
56
+ }
package/lib/screens.js ADDED
@@ -0,0 +1,8 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
+
5
+ export { createScreens } from './globals.js';
6
+ export { default as FoodVendorScreen } from './screens/food-vendor-screen.jsx';
7
+ export { default as FoodMenuScreen } from './screens/food-menu-screen.jsx';
8
+ export { default as QRCodeScreen } from './screens/qr-code-screen.jsx';
@@ -0,0 +1,94 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
+
5
+ import Module from 'module';
6
+
7
+ const reactNativeMock = {
8
+ Linking: {
9
+ async openURL() {
10
+ return true;
11
+ },
12
+ async canOpenURL() {
13
+ return true;
14
+ },
15
+ },
16
+ Platform: {
17
+ OS: 'android',
18
+ },
19
+ };
20
+
21
+ let originalLoad;
22
+ let isInstalled = false;
23
+
24
+ /**
25
+ * Install a Node-level mock for `react-native`.
26
+ *
27
+ * This is intended for TAP / Node-based unit tests where React Native
28
+ * is not available as a runtime module.
29
+ *
30
+ * @returns {object}
31
+ * Returns the active mock object so tests can override behavior.
32
+ */
33
+ export function mockReactNative() {
34
+ if (isInstalled) {
35
+ return reactNativeMock;
36
+ }
37
+
38
+ originalLoad = Module._load;
39
+
40
+ Module._load = function (request, parent, isMain) {
41
+ if (request === 'react-native') {
42
+ return reactNativeMock;
43
+ }
44
+
45
+ return originalLoad.call(this, request, parent, isMain);
46
+ };
47
+
48
+ isInstalled = true;
49
+
50
+ return reactNativeMock;
51
+ }
52
+
53
+ /**
54
+ * Restore the original Node module loader.
55
+ */
56
+ export function unmockReactNative() {
57
+ if (!isInstalled) {
58
+ return;
59
+ }
60
+
61
+ Module._load = originalLoad;
62
+ originalLoad = null;
63
+ isInstalled = false;
64
+ }
65
+
66
+ /**
67
+ * Reset the mock back to defaults between tests.
68
+ *
69
+ * @returns {object}
70
+ * Returns the reset mock object.
71
+ */
72
+ export function resetReactNativeMock() {
73
+ reactNativeMock.Linking.openURL = async function () {
74
+ return true;
75
+ };
76
+
77
+ reactNativeMock.Linking.canOpenURL = async function () {
78
+ return true;
79
+ };
80
+
81
+ reactNativeMock.Platform.OS = 'android';
82
+
83
+ return reactNativeMock;
84
+ }
85
+
86
+ /**
87
+ * Get the active mock object.
88
+ *
89
+ * @returns {object}
90
+ */
91
+ export function getReactNativeMock() {
92
+ return reactNativeMock;
93
+ }
94
+
@@ -0,0 +1,82 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
+
5
+ /**
6
+ * Mix two hex colors.
7
+ *
8
+ * @param {string} a
9
+ * First hex color, like "#ff0000".
10
+ * @param {string} b
11
+ * Second hex color, like "#0000ff".
12
+ * @param {number} amount
13
+ * Amount of b to mix in, from 0 to 1.
14
+ *
15
+ * @returns {string}
16
+ * Mixed hex color.
17
+ */
18
+ export function mixHexColors(a, b, amount = 0.5) {
19
+ const ah = a.replace('#', '');
20
+ const bh = b.replace('#', '');
21
+
22
+ const ar = parseInt(ah.slice(0, 2), 16);
23
+ const ag = parseInt(ah.slice(2, 4), 16);
24
+ const ab = parseInt(ah.slice(4, 6), 16);
25
+
26
+ const br = parseInt(bh.slice(0, 2), 16);
27
+ const bg = parseInt(bh.slice(2, 4), 16);
28
+ const bb = parseInt(bh.slice(4, 6), 16);
29
+
30
+ const r = Math.round(ar + (br - ar) * amount);
31
+ const g = Math.round(ag + (bg - ag) * amount);
32
+ const b2 = Math.round(ab + (bb - ab) * amount);
33
+
34
+ return (
35
+ '#' +
36
+ r.toString(16).padStart(2, '0') +
37
+ g.toString(16).padStart(2, '0') +
38
+ b2.toString(16).padStart(2, '0')
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Return black or white depending on which has better contrast.
44
+ *
45
+ * @param {string} hex
46
+ * Background color as "#rrggbb".
47
+ *
48
+ * @returns {string}
49
+ * "#000000" or "#ffffff".
50
+ */
51
+ export function contrastColor(hex) {
52
+ const value = hex.replace('#', '');
53
+
54
+ const r = parseInt(value.slice(0, 2), 16) / 255;
55
+ const g = parseInt(value.slice(2, 4), 16) / 255;
56
+ const b = parseInt(value.slice(4, 6), 16) / 255;
57
+
58
+ const luminance =
59
+ 0.2126 * toLinear(r) +
60
+ 0.7152 * toLinear(g) +
61
+ 0.0722 * toLinear(b);
62
+
63
+ return luminance > 0.179 ? '#000000' : '#ffffff';
64
+ }
65
+
66
+ /**
67
+ * Convert sRGB channel to linear RGB.
68
+ *
69
+ * @param {number} channel
70
+ * sRGB channel from 0 to 1.
71
+ *
72
+ * @returns {number}
73
+ * Linear RGB channel.
74
+ */
75
+ export function toLinear(channel) {
76
+ if (channel <= 0.03928) {
77
+ return channel / 12.92;
78
+ }
79
+
80
+ return ((channel + 0.055) / 1.055) ** 2.4;
81
+ }
82
+
@@ -0,0 +1,19 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
+
5
+ /**
6
+ * Format a numeric price for display.
7
+ *
8
+ * @param {number|string} price
9
+ * @returns {string}
10
+ */
11
+ export function formatPrice(price) {
12
+ const num = parseFloat(price);
13
+
14
+ if (Number.isNaN(num)) {
15
+ return '';
16
+ }
17
+
18
+ return num % 1 === 0 ? num.toString() : num.toFixed(2);
19
+ }
@@ -0,0 +1,38 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
+
5
+ /**
6
+ * Pick a different header image every day
7
+ *
8
+ *
9
+ * const headerImages = [
10
+ * require('@/assets/images/main.webp'),
11
+ * require('@/assets/images/main-2.webp'),
12
+ * require('@/assets/images/main-3.webp'),
13
+ * ];
14
+ *
15
+ * @param {images} price
16
+ * @returns {image}
17
+ */
18
+ export function pickDailyImage(images) {
19
+ const now = new Date();
20
+ const start = new Date(now.getFullYear(), 0, 0);
21
+ const day = Math.floor((now - start) / 86400000);
22
+
23
+ return images[day % images.length];
24
+ }
25
+
26
+
27
+ /**
28
+ * Pick a random image.
29
+ *
30
+ * @param {Array} images
31
+ * List of static require() image values.
32
+ *
33
+ * @returns {*}
34
+ * Selected image.
35
+ */
36
+ export function pickRandomImage(images) {
37
+ return images[Math.floor(Math.random() * images.length)];
38
+ }
@@ -0,0 +1,94 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
+
5
+ import { Linking, Platform } from 'react-native';
6
+
7
+ /**
8
+ * Open a plain URL.
9
+ */
10
+ export async function openUrl(url) {
11
+ if (!url) return false;
12
+
13
+ try {
14
+ await Linking.openURL(url);
15
+ return true;
16
+ }
17
+ catch (e) {
18
+ console.error('openUrl failed:', e);
19
+ return false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Open an app via deep link with fallback to store.
25
+ *
26
+ * @param {object} opts
27
+ * @param {string} [opts.url] Deep link (optional)
28
+ * @param {string} [opts.packageName] Android package
29
+ * @param {string} [opts.appStoreUrl] iOS fallback
30
+ */
31
+ export async function openApp({
32
+ url,
33
+ packageName,
34
+ appStoreUrl,
35
+ }) {
36
+ try {
37
+ if (Platform.OS === 'android') {
38
+ if (!packageName) {
39
+ if (url) return openUrl(url);
40
+ return false;
41
+ }
42
+
43
+ const appUrl = url || `${packageName}://`;
44
+
45
+ try {
46
+ const canOpen = await Linking.canOpenURL(appUrl);
47
+
48
+ if (canOpen) {
49
+ await Linking.openURL(appUrl);
50
+ return true;
51
+ }
52
+ }
53
+ catch (_) {
54
+ }
55
+
56
+ // fallback → Play Store
57
+ await Linking.openURL(
58
+ `market://details?id=${packageName}`
59
+ );
60
+ return true;
61
+ }
62
+
63
+ if (Platform.OS === 'ios') {
64
+ if (url) {
65
+ await Linking.openURL(url);
66
+ return true;
67
+ }
68
+
69
+ if (appStoreUrl) {
70
+ await Linking.openURL(appStoreUrl);
71
+ return true;
72
+ }
73
+ }
74
+
75
+ return false;
76
+ }
77
+ catch (e) {
78
+ console.error('openApp failed:', e);
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Resolve + open whatever makes sense.
85
+ *
86
+ * Use this as your "one call".
87
+ */
88
+ export async function openExternal(opts) {
89
+ if (opts.packageName || opts.appStoreUrl) {
90
+ return openApp(opts);
91
+ }
92
+
93
+ return openUrl(opts.url);
94
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "author": {
3
+ "email": "wesley@opndev.io",
4
+ "name": "Wesley Schwengle"
5
+ },
6
+ "description": "React Native code for events",
7
+ "devDependencies": {
8
+ "jsdoc": "latest",
9
+ "tap": "latest"
10
+ },
11
+ "exports": {
12
+ ".": "./lib/index.js",
13
+ "./components": "./lib/components.js",
14
+ "./globals": "./lib/screen-registery.js",
15
+ "./hero-screen-registry": "./lib/hero-screen-registery.js",
16
+ "./lite": "./lib/hero-screen-registery.js",
17
+ "./notifications": "./lib/notifications.js",
18
+ "./notifications-fcm": "./lib/notifications/fcm.js",
19
+ "./screen-registery": "./lib/screen-registery.js",
20
+ "./screens": "./lib/screens.js"
21
+ },
22
+ "keywords": [],
23
+ "license": "GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions",
24
+ "main": "lib/index.js",
25
+ "name": "@opndev/react-native-events",
26
+ "private": false,
27
+ "scripts": {
28
+ "build": "rzil build",
29
+ "jsdoc": "jsdoc -c .jsdoc.json",
30
+ "pkg": "rzil pkg",
31
+ "release": "rzil release",
32
+ "test": "tap"
33
+ },
34
+ "sideEffects": false,
35
+ "type": "module",
36
+ "version": "0.0.10"
37
+ }