浏览代码

MIGRATION TO EXPO

Alex Trofimov 10 月之前
父节点
当前提交
cd7c20a6d8
共有 100 个文件被更改,包括 1661 次插入 和 2190 次删除
  1. 0 2
      .bundle/config
  2. 0 4
      .eslintrc.js
  3. 35 61
      .gitignore
  4. 0 1
      .node-version
  5. 0 7
      .prettierrc.js
  6. 1 0
      .vscode/extensions.json
  7. 7 0
      .vscode/settings.json
  8. 0 1
      .watchmanconfig
  9. 0 875
      App.js
  10. 0 118
      App.tsx
  11. 0 46
      ErrorPage.js
  12. 0 6
      Gemfile
  13. 0 98
      Gemfile.lock
  14. 0 263
      Icons.js
  15. 33 69
      README.md
  16. 0 59
      Theme.js
  17. 0 34
      VideoCall.js
  18. 0 14
      __tests__/App-test.tsx
  19. 80 76
      android/app/build.gradle
  20. 4 0
      android/app/proguard-rules.pro
  21. 1 7
      android/app/src/debug/AndroidManifest.xml
  22. 0 75
      android/app/src/debug/java/com/una/ReactNativeFlipper.java
  23. 31 37
      android/app/src/main/AndroidManifest.xml
  24. 0 41
      android/app/src/main/java/com/una/android/MainActivity.java
  25. 0 62
      android/app/src/main/java/com/una/android/MainApplication.java
  26. 二进制
      android/app/src/main/res/drawable-hdpi/splash_image.png
  27. 二进制
      android/app/src/main/res/drawable-mdpi/splash_image.png
  28. 二进制
      android/app/src/main/res/drawable-xhdpi/splash_image.png
  29. 二进制
      android/app/src/main/res/drawable-xxhdpi/splash_image.png
  30. 3 2
      android/app/src/main/res/drawable/rn_edit_text_material.xml
  31. 0 11
      android/app/src/main/res/drawable/splashscreen.xml
  32. 0 15
      android/app/src/main/res/layout/launch_screen.xml
  33. 二进制
      android/app/src/main/res/mipmap-hdpi-v26/ic_foreground.png
  34. 0 4
      android/app/src/main/res/mipmap-hdpi-v26/ic_launcher.xml
  35. 二进制
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  36. 二进制
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  37. 二进制
      android/app/src/main/res/mipmap-ldpi-v26/ic_foreground.png
  38. 0 4
      android/app/src/main/res/mipmap-ldpi-v26/ic_launcher.xml
  39. 二进制
      android/app/src/main/res/mipmap-ldpi/ic_launcher.png
  40. 二进制
      android/app/src/main/res/mipmap-mdpi-v26/ic_foreground.png
  41. 0 4
      android/app/src/main/res/mipmap-mdpi-v26/ic_launcher.xml
  42. 二进制
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  43. 二进制
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  44. 二进制
      android/app/src/main/res/mipmap-xhdpi-v26/ic_foreground.png
  45. 0 4
      android/app/src/main/res/mipmap-xhdpi-v26/ic_launcher.xml
  46. 二进制
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  47. 二进制
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  48. 二进制
      android/app/src/main/res/mipmap-xxhdpi-v26/ic_foreground.png
  49. 0 4
      android/app/src/main/res/mipmap-xxhdpi-v26/ic_launcher.xml
  50. 二进制
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  51. 二进制
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  52. 二进制
      android/app/src/main/res/mipmap-xxxhdpi-v26/ic_foreground.png
  53. 0 4
      android/app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher.xml
  54. 二进制
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  55. 二进制
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  56. 0 3
      android/app/src/main/res/values/colors-icon.xml
  57. 0 4
      android/app/src/main/res/values/colors-splash.xml
  58. 5 2
      android/app/src/main/res/values/strings.xml
  59. 14 13
      android/app/src/main/res/values/styles.xml
  60. 0 20
      android/app/src/release/java/com/una/ReactNativeFlipper.java
  61. 19 16
      android/build.gradle
  62. 26 10
      android/gradle.properties
  63. 二进制
      android/gradle/wrapper/gradle-wrapper.jar
  64. 3 1
      android/gradle/wrapper/gradle-wrapper.properties
  65. 32 15
      android/gradlew
  66. 94 89
      android/gradlew.bat
  67. 38 3
      android/settings.gradle
  68. 65 3
      app.json
  69. 24 0
      app/1111_layout.tsx
  70. 225 0
      app/index.tsx
  71. 29 0
      app/modal.tsx
  72. 二进制
      assets/images/android-icon-background.png
  73. 二进制
      assets/images/android-icon-foreground.png
  74. 二进制
      assets/images/android-icon-monochrome.png
  75. 二进制
      assets/images/favicon.png
  76. 二进制
      assets/images/icon.png
  77. 二进制
      assets/images/partial-react-logo.png
  78. 二进制
      assets/images/react-logo.png
  79. 二进制
      assets/images/react-logo@2x.png
  80. 二进制
      assets/images/react-logo@3x.png
  81. 二进制
      assets/images/splash-icon.png
  82. 0 3
      babel.config.js
  83. 99 0
      components/bottom-tabs.tsx
  84. 25 0
      components/external-link.tsx
  85. 18 0
      components/haptic-tab.tsx
  86. 60 0
      components/header.tsx
  87. 19 0
      components/hello-wave.tsx
  88. 181 0
      components/icons.tsx
  89. 79 0
      components/parallax-scroll-view.tsx
  90. 60 0
      components/themed-text.tsx
  91. 14 0
      components/themed-view.tsx
  92. 45 0
      components/ui/collapsible.tsx
  93. 32 0
      components/ui/icon-symbol.ios.tsx
  94. 41 0
      components/ui/icon-symbol.tsx
  95. 44 0
      components/web-view-error-screen.tsx
  96. 44 0
      components/web-view-load-with-request.tsx
  97. 50 0
      components/web-view.tsx
  98. 18 0
      constants/settings.ts
  99. 53 0
      constants/theme.ts
  100. 10 0
      eslint.config.js

+ 0 - 2
.bundle/config

@@ -1,2 +0,0 @@
-BUNDLE_PATH: "vendor/bundle"
-BUNDLE_FORCE_RUBY_PLATFORM: 1

+ 0 - 4
.eslintrc.js

@@ -1,4 +0,0 @@
-module.exports = {
-  root: true,
-  extends: '@react-native-community',
-};

+ 35 - 61
.gitignore

@@ -1,69 +1,43 @@
-# OSX
-#
-.DS_Store
-
-# Xcode
-#
-build/
-*.pbxuser
-!default.pbxuser
-*.mode1v3
-!default.mode1v3
-*.mode2v3
-!default.mode2v3
-*.perspectivev3
-!default.perspectivev3
-xcuserdata
-*.xccheckout
-*.moved-aside
-DerivedData
-*.hmap
-*.ipa
-*.xcuserstate
-ios/.xcode.env.local
+# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
 
-# Android/IntelliJ
-#
-build/
-.idea
-.gradle
-local.properties
-*.iml
-*.hprof
-.cxx/
-*.keystore
-!debug.keystore
-
-# node.js
-#
+# dependencies
 node_modules/
-npm-debug.log
-yarn-error.log
 
-# fastlane
-#
-# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
-# screenshots whenever they are needed.
-# For more information about the recommended setup visit:
-# https://docs.fastlane.tools/best-practices/source-control/
+# Expo
+.expo/
+dist/
+web-build/
+expo-env.d.ts
+
+# Native
+.kotlin/
+*.orig.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+
+# Metro
+.metro-health-check*
 
-**/fastlane/report.xml
-**/fastlane/Preview.html
-**/fastlane/screenshots
-**/fastlane/test_output
+# debug
+npm-debug.*
+yarn-debug.*
+yarn-error.*
 
-# Bundle artifact
-*.jsbundle
+# macOS
+.DS_Store
+*.pem
 
-# Ruby / CocoaPods
-/ios/Pods/
-/vendor/bundle/
+# local env files
+.env*.local
 
-# Temporary files created by Metro to check the health of the file watcher
-.metro-health-check*
+# typescript
+*.tsbuildinfo
+
+app-example
 
-# BUCK
-buck-out/
-\.buckd/
-*.keystore
-!debug.keystore
+# generated native folders
+/ios
+/android

+ 0 - 1
.node-version

@@ -1 +0,0 @@
-18

+ 0 - 7
.prettierrc.js

@@ -1,7 +0,0 @@
-module.exports = {
-  arrowParens: 'avoid',
-  bracketSameLine: true,
-  bracketSpacing: false,
-  singleQuote: true,
-  trailingComma: 'all',
-};

+ 1 - 0
.vscode/extensions.json

@@ -0,0 +1 @@
+{ "recommendations": ["expo.vscode-expo-tools"] }

+ 7 - 0
.vscode/settings.json

@@ -0,0 +1,7 @@
+{
+  "editor.codeActionsOnSave": {
+    "source.fixAll": "explicit",
+    "source.organizeImports": "explicit",
+    "source.sortMembers": "explicit"
+  }
+}

+ 0 - 1
.watchmanconfig

@@ -1 +0,0 @@
-{}

+ 0 - 875
App.js

@@ -1,875 +0,0 @@
-/**
- * Copyright (c) UNA, Inc - https://unacms.com
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @format
- * @flow strict-local
- */
-
-import React, { Component } from 'react';
-import OneSignal from 'react-native-onesignal';
-import SplashScreen from 'react-native-splash-screen';
-import { WebView } from 'react-native-webview';
-import {
-    Platform,
-    StyleSheet,
-    Linking,
-    BackHandler,
-    ActivityIndicator,
-    Alert,
-    Appearance,
-    PermissionsAndroid,
-    NativeModules,
-} from 'react-native';
-import {
-    Link,
-    Text,
-    HStack,
-    Center,
-    Heading,
-    Switch,
-    useColorMode,
-    NativeBaseProvider,
-    VStack,
-    Box,
-    View,
-    Drawer,
-    Image,
-    Button,
-    Input,
-    Icon,
-    IconButton,
-    Pressable,
-    StatusBar,
-    SearchIcon,
-} from 'native-base';
-import NativeBaseIcon from './src/components/NativeBaseIcon';
-import { Path, G } from "react-native-svg";
-
-/*
-import RNIap, {
-  purchaseErrorListener,
-  purchaseUpdatedListener,
-  type ProductPurchase,
-  type PurchaseError
-} from 'react-native-iap';
-
-import JitsiMeet, { JitsiMeetView } from 'react-native-jitsi-meet';
-import VideoCall from './VideoCall';
-*/
-import ErrorPage from './ErrorPage';
-import * as Icons from './Icons';
-import { themeDark, themeLight, useTheme } from './Theme';
-
-import { version } from './package.json';
-
-type Props = {};
-
-const BASE_URL = 'https://unacms.com/'; // site URL
-const MIX_LIGHT = '0'; // template styles mix for light mode
-const MIX_DARK = '0'; // template styles mix for dark mode
-const TEMPLATE = 'artificer'; // template name
-const TITLE = 'UNA CMS | Community Management System'; // homepage title
-const ONESIGNALAPPID = ''; // you can obtain one from https://onesignal.com/
-const PAYMENTS_CALLBACK = ''; // empty string means payment functionality is disabled
-
-const requestPermissions = async () => {
-    try {
-        const oGranted = await PermissionsAndroid.requestMultiple([
-            PermissionsAndroid.PERMISSIONS.CAMERA,
-            PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
-            PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
-            PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
-        ]);
-        
-        for (const [sKey, sValue] of Object.entries(oGranted)) {
-            console.log(`Permission - ${sKey}: ${sValue}`);
-        }
-    } catch (err) {
-        console.warn(err);
-    }
-};
-
-export default class App extends Component<Props> {
-
-    loading = false;
-    videoCallAudio = false;
-    purchaseUpdateSubscription = null;
-    purchaseErrorSubscription = null;
-    products = {};
-    url = '';
-
-    constructor(props) {
-        super(props);
-
-        var colorScheme = Appearance.getColorScheme();
-        this.url = `${BASE_URL}?skin=${TEMPLATE}&mix=${'dark' === colorScheme ? MIX_DARK : MIX_LIGHT}`;
-
-        this.state = {
-            status: '',
-            backButtonEnabled: false,
-            forwardButtonEnabled: false,
-            loading: this.loading,
-            data: {},
-            searchbar: false,
-            key: 1,
-            videoCall: 0,
-            videoCallUri: false,
-            colorScheme: colorScheme,
-            drawerOpen: false,
-        };
-
-        this.onBack = this.onBack.bind(this);
-        this.onMainMenu = this.onMainMenu.bind(this);
-        this.onBackAndMainMenu = this.onBackAndMainMenu.bind(this);
-        this.onSearchMenu = this.onSearchMenu.bind(this);
-        this.onNotificationsMenu = this.onNotificationsMenu.bind(this);
-        this.onAddMenu = this.onAddMenu.bind(this);
-        this.onMessengerMenu = this.onMessengerMenu.bind(this);
-        this.onProfileMenu = this.onProfileMenu.bind(this);
-        this.onSearchCancelMenu = this.onSearchCancelMenu.bind(this);
-        this.onSearch = this.onSearch.bind(this);
-
-        this.onConferenceTerminated = this.onConferenceTerminated.bind(this);
-        this.onConferenceJoined = this.onConferenceJoined.bind(this);
-        this.onConferenceWillJoin = this.onConferenceWillJoin.bind(this);
-
-        this.onVideoCallStart = this.onVideoCallStart.bind(this);
-
-        this.onDrawerToggle = this.onDrawerToggle.bind(this);
-    }
-/*
-    async onRequestPurchase (sku: string) {
-        if ('ios' === Platform.OS)
-            RNIap.clearTransactionIOS();
-        try {
-            await RNIap.requestPurchase(sku, false);
-        } catch (err) {
-            console.warn(err.code, err.message);
-            Alert.alert(err.message);
-        }
-    }
-
-    async onRequestSubscription (sku: string) {
-        if ('ios' === Platform.OS)
-            RNIap.clearTransactionIOS();
-        try {            
-            await RNIap.requestSubscription(sku);
-        } catch (err) {
-            console.warn(err.code, err.message);
-            Alert.alert(err.message);
-        }
-    }
-*/
-    onConferenceTerminated(e) {
-        this.endVideoCall();        
-        this.injectJavaScript("bx_mobile_apps_video_terminated(" + JSON.stringify(e.nativeEvent) + ")");
-    }
-
-    onConferenceJoined(e) {
-        this.injectJavaScript("bx_mobile_apps_video_joined(" + JSON.stringify(e.nativeEvent) + ")");
-    }
-
-    onConferenceWillJoin(e) {
-        this.injectJavaScript("bx_mobile_apps_video_will_join(" + JSON.stringify(e.nativeEvent) + ")");
-    }
-
-    endVideoCall() {
-        this.videoCallAudio = false;
-        this.setState ({
-            videoCall: 0,
-            videoCallUri: false,
-        });
-        //JitsiMeet.endCall();
-    }    
-
-    onVideoCallStart(sUri, bAudioOnly = false) {
-        if (!this.state.videoCall) {
-            this.videoCallAudio = bAudioOnly;
-            this.setState ({
-                videoCall: 1,
-                videoCallUri: sUri,
-            });
-        }
-    }
-
-    onVideoCallToggle() {
-        if (this.state.videoCall) {
-            this.endVideoCall();
-        }
-        else {
-            this.onVideoCallStart('testing-egr58t32g2');
-        }
-    }
-
-    onBack() {
-        this.endVideoCall();
-        if (this.state.backButtonEnabled) {
-    
-            if ('android' === Platform.OS)
-                this.onWebViewLoadStart(); // show loading screen
-            
-            this.refs.webView1.goBack();
-            // this.injectJavaScript("window.history.go(-1)");
-            return true;
-        }
-        return false;
-    }
-    
-    onMainMenu() {
-        this.endVideoCall();
-        if (this.state.data.loggedin)
-            this.injectJavaScript("bx_mobile_apps_show_main_menu()");
-        else
-            this.drawerOpen();
-    }
-
-    onBackAndMainMenu() {
-        if (this.state.backButtonEnabled) {
-            return this.onBack();
-        }
-        else {
-            return this.onMainMenu();
-        }
-    }
-
-    onProfileMenu() {
-        this.endVideoCall();
-        this.injectJavaScript("bx_mobile_apps_show_profile_menu()");
-        return true;
-    }
-
-    onAddMenu() {
-        this.endVideoCall();
-        this.injectJavaScript("bx_mobile_apps_show_add_menu()");
-        return true;
-    }
-    
-    onNotificationsMenu() {
-        this.endVideoCall();
-        this.injectJavaScript(`document.location = "${BASE_URL}page.php?i=notifications-view"`);
-        return true;
-    }
-    
-    onMessengerMenu() {
-        this.endVideoCall();
-        this.injectJavaScript("bx_mobile_apps_show_messenger_menu()");
-        return true;
-    }
-    
-    onHomeMenu() {
-        this.endVideoCall();
-        if (`${BASE_URL}` == this.url || this.url.startsWith(`${BASE_URL}?skin=${TEMPLATE}`) || `${BASE_URL}index.php` == this.url) {        
-            this.injectJavaScript("bx_mobile_apps_close_sliding_menus()");
-        } else {
-            this.setState ({
-                url: `${BASE_URL}?skin=${TEMPLATE}&mix=${'dark' == this.state.colorScheme ? MIX_DARK : MIX_LIGHT}`,
-                status: '',
-                searchbar: false,
-                key: this.state.key + 1,
-            });
-        }
-
-        return true;
-    }
-
-    onSearch(event) {
-        this.onSearchCancelMenu();        
-        this.injectJavaScript(`window.location = '${BASE_URL}searchKeyword.php?keyword=${event.nativeEvent.text}';`);
-    }
-
-    onSearchCancelMenu() {
-        this.setState ({
-            loading: this.loading,
-            searchbar: false,
-        });
-        return true;
-    }
-
-    onSearchMenu() {
-        this.endVideoCall();
-        this.setState ({
-            loading: this.loading,
-            searchbar: true,
-        });
-        return true;
-    }
-
-    onDrawerLoginMenu() {
-        this.onHomeMenu();
-        this.drawerClose();
-        return true;
-    }
-
-    onDrawerJoinMenu() {
-        this.injectJavaScript(`window.location = '${BASE_URL}page/create-account';`);
-        this.drawerClose();
-        return true;
-    }
-    
-    onDrawerForgotMenu() {
-        this.injectJavaScript(`window.location = '${BASE_URL}page/forgot-password';`);
-        this.drawerClose();
-        return true;
-    }
-
-    onWebViewNavigationStateChange(navState) {
-
-        if (-1 != navState.url.indexOf('#') && this.url == navState.url.substring(0, navState.url.indexOf('#'))) {
-            this.onWebViewLoadEnd();
-            return;   
-        }
-
-        this.url = navState.url;
-        this.setState ({
-            status: navState.title,
-            backButtonEnabled: navState.canGoBack,
-            forwardButtonEnabled: navState.canGoForward,
-            searchbar: false,
-        });
-    }
-
-    onWebViewLoadStart (syntheticEvent) {
-        var sUrl = 'undefined' !== typeof(syntheticEvent) && 'undefined' !== typeof(syntheticEvent['nativeEvent']) && 'undefined' !== typeof(syntheticEvent['nativeEvent']['url']) ? syntheticEvent.nativeEvent.url : false;
-
-        if (sUrl && -1 == sUrl.indexOf(`${BASE_URL}`)) { // loading indicator is supported on original domains only
-            this.onWebViewLoadEnd(syntheticEvent);
-            return;
-        }
-
-        this.loading = true;
-        this.setState ({
-            loading: this.loading,
-            searchbar: false,
-        });
-    }
-    
-    onWebViewLoadEnd (syntheticEvent) {
-
-        this.loading = false;
-        this.setState ({
-            loading: this.loading,
-            searchbar: false,
-        });
-    }
-
-    onWebViewRenderError () {
-        return (
-            <ErrorPage onReload={() => (this.reload())}></ErrorPage>
-        );
-    }
-
-    reload() {
-        this.refs.webView1.reload();
-    }
-
-    postMessage (data) {
-        // posts a message to web view
-        this.refs.webView1.postMessage(data);
-    }
-
-    injectJavaScript (script) {
-        // executes JavaScript immediately in web view
-        this.refs.webView1.injectJavaScript(script);
-    }
-
-    onWebViewShouldStartLoadWithRequest (event) {
-
-        var aExceptionsUrls = [
-            'redirect_uri=', 'redirect_uri%', 'redirectUri%', // Facebook/LinkedIn Connect and other Connect apps
-            'signin/oauth', 'signin%2Foauth', // Google Connect
-            '/m/oauth2', // Dolphin/UNA Connect
-            'api.twitter.com', // Twitter Connect
-        ];
-        var bExceptionUrl = aExceptionsUrls.some(e => {
-            return -1 !== event.url.indexOf(e);
-        });
-
-        if ('about:blank' === event.url)
-            return false;
-
-        if ((0 != event.url.indexOf('http') || (!bExceptionUrl && -1 == event.url.indexOf(`${BASE_URL}`) && ('android' === Platform.OS || ('click' == event.navigationType && 'ios' === Platform.OS)))) || -1 != event.url.indexOf('calendar_sync')) {
-
-            Linking.canOpenURL(event.url).then(supported => {
-                if (supported) {
-                    Linking.openURL(event.url);
-                } 
-                else {
-                    console.log('Don\'t know how to open URI: ' + event.url);
-                }
-                return false;
-            });
-            return false;
-        }
-
-        if ('android' === Platform.OS)
-            this.onWebViewLoadStart(); // show loading screen
-
-        return true;
-    }
-  
-    javascriptToInject () { 
-        return 'glBxNexusApp = ' + JSON.stringify({ver:version});
-    }
-
-    componentDidCatch(error, info) {
-        SplashScreen.hide();
-    }
-
-    async componentDidMount() {
-
-        Appearance.addChangeListener(({ colorScheme }) => { // not working in Android :(
-            var s = Appearance.getColorScheme(); // colorScheme param is working incorrect in iOS13, so we need to get it explicitly
-            if (s != this.state.colorScheme) {
-                var sUrl = this.url;
-                var sUri = `skin=${TEMPLATE}&mix=${'dark' === s ? MIX_DARK : MIX_LIGHT}`;
-                sUrl += (-1 === sUrl.indexOf('?') ? '?' : '&') + sUri;
-                this.setState ({
-                    colorScheme: s,
-                    url: sUrl,
-                });
-            }
-        });
-
-        OneSignal.setLogLevel(6, 0);
-        OneSignal.setAppId(ONESIGNALAPPID);
-        OneSignal.promptForPushNotificationsWithUserResponse(response => {
-            console.log("OneSignal Prompt response:", response);
-        });
-        OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent => {
-            console.log("OneSignal: notification in foreground:", notificationReceivedEvent);
-        });
-
-        OneSignal.setNotificationOpenedHandler(openResult => {
-            console.log("OneSignal: notification opened:", openResult);
-
-            if ('undefined' !== typeof(openResult.notification.additionalData) && 'undefined' !== typeof(openResult.notification.additionalData.url)) {
-                this.injectJavaScript(`window.location = '${openResult.notification.additionalData.url}';`);
-            }            
-        });
-
-        const deviceState = await OneSignal.getDeviceState();
-        this.setState({
-            isSubscribed : deviceState.isSubscribed
-        });
-
-        if (Platform.OS === 'android') {
-            this.backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
-                if (this.state.backButtonEnabled) {
-                    this.onBack(); // works best when the goBack is async
-                    return true;
-                }
-                else {
-                    return false;
-                }
-            });
-        }        
-/*
-        if (PAYMENTS_CALLBACK != '') {
-            var _self = this;
-            try {
-                await RNIap.initConnection();
-                const sProducts = await fetch(BASE_URL + 'modules/?r=oauth2/com/get_products_names&module=system&class=BaseServices');
-                _self.products = await sProducts.json();
-                await RNIap.getSubscriptions(Object.keys(_self.products));
-                await RNIap.getProducts(Object.keys(_self.products));
-            } catch(err) {
-                console.warn(err);
-            }
-
-            purchaseUpdateSubscription = purchaseUpdatedListener(async (oPurchaseData: ProductPurchase) => {
-                console.log('purchaseUpdatedListener', oPurchaseData);
-
-                if ('undefined' !== typeof(oPurchaseData.transactionId) && _self.state.data.loggedin != 0) {
-                    var sProductName = 'undefined' !== typeof(oPurchaseData.productId) ? oPurchaseData.productId : null;
-                    var sTxId = 'undefined' !== typeof(oPurchaseData.transactionId) ? oPurchaseData.transactionId : '';
-                    var sTxIdOriginal = 'undefined' !== typeof(oPurchaseData.originalTransactionIdentifierIOS) ? oPurchaseData.originalTransactionIdentifierIOS : '';
-
-                    await fetch(PAYMENTS_CALLBACK, {
-                        method: 'POST',
-                        headers: {
-                            Accept: 'application/json',
-                            'Content-Type': 'application/json',
-                            'User-Agent': "UNAMobileApp/Mobile (" + Platform.OS + ")",
-                        },
-                        body: JSON.stringify({
-                            notification_type: 'INITIAL_BUY_CUSTOM',
-                            module: 'undefined' !== typeof(_self.products[sProductName]) ? _self.products[sProductName] : null,
-                            product: sProductName,
-                            count: 1,
-                            original_transaction: sTxIdOriginal,
-                            transaction: sTxId,
-                            profile_id: _self.state.data.loggedin ? _self.state.data.user_info.id : 0,
-                            original_data: oPurchaseData,
-                        })
-                    });
-    
-                    if ('ios' === Platform.OS)
-                        RNIap.finishTransactionIOS(sTxId);
-                }
-            });
-            purchaseErrorSubscription = purchaseErrorListener((error: PurchaseError) => {
-                console.log('purchaseErrorListener', error);
-            }); 
-        }
-*/
-        if (Platform.OS === 'android')
-            requestPermissions();
-
-        SplashScreen.hide();
-    }
-    
-    componentWillUnmount() {
-
-        if (this.purchaseUpdateSubscription) {
-            this.purchaseUpdateSubscription.remove();
-            this.purchaseUpdateSubscription = null;
-        }
-        if (this.purchaseErrorSubscription) {
-            this.purchaseErrorSubscription.remove();
-            this.purchaseErrorSubscription = null;
-        }
-
-        if (Platform.OS === 'android') {
-            this.backHandler.remove();
-        }
-    }
-        
-    onWebViewMessage(event) {
-
-        let oMsgData;
-        try {
-            oMsgData = JSON.parse(event.nativeEvent.data);
-        }
-        catch(err) {
-            console.warn(err);
-            return;
-        }
-
-        if ('undefined' !== typeof(oMsgData.loggedin)) {
-            this.setState ({
-                loading: this.loading,
-                data: oMsgData,
-                searchbar: false,
-                key: 'undefined' === typeof(this.state.data.loggedin) || this.state.data.loggedin != oMsgData.loggedin ? this.state.key + 1 : this.state.key,
-            });
-        }
-
-        if ('undefined' !== typeof(oMsgData['push_tags']) && oMsgData['push_tags'] !== false) {
-
-            if ('undefined' !== typeof(oMsgData['push_tags']['user']) && oMsgData['push_tags']['user'].length) {
-                console.log('User ID: ' + JSON.stringify(oMsgData['push_tags']['user']));
-                OneSignal.setExternalUserId(oMsgData['push_tags']['user']);
-            }
-
-            if ('undefined' !== typeof(oMsgData['push_tags']['email']) && oMsgData['push_tags']['email'].length) {
-                OneSignal.setEmail(oMsgData['push_tags']['email'], oMsgData['push_tags']['email_hash'], (sError) => {
-                    if ('undefined' !== typeof(sError))
-                        console.warn("OneSignal set email error: " + sError);
-                });
-                delete oMsgData['push_tags']['email'];
-                delete oMsgData['push_tags']['email_hash'];
-            }
-            OneSignal.sendTags(oMsgData['push_tags']);
-        }
-
-        if ('undefined' !== typeof(oMsgData['stop_loading']) && oMsgData['stop_loading']) {
-            // this.onWebViewLoadEnd (event);
-        }
-
-        if ('undefined' !== typeof(oMsgData['video_call_start'])) {
-            this.onVideoCallStart (oMsgData['video_call_start']['uri'], 'undefined' === typeof(oMsgData['video_call_start']['audio']) ? false : oMsgData['video_call_start']['audio']);
-        }
-        if ('undefined' !== typeof(oMsgData['video_call_stop']) && oMsgData['video_call_stop']) {
-            this.endVideoCall ();
-        }
-
-        if ('undefined' !== typeof(oMsgData['request_purchase']) && oMsgData['request_purchase']) {
-            this.onRequestPurchase (oMsgData['request_purchase']);
-        }
-        if ('undefined' !== typeof(oMsgData['request_subscription']) && oMsgData['request_subscription']) {
-            this.onRequestSubscription (oMsgData['request_subscription']);
-        }
-
-        if ('undefined' !== typeof(oMsgData['goto_home']) && oMsgData['goto_home']) {
-            this.onHomeMenu ();
-        }
-        if ('undefined' !== typeof(oMsgData['reload']) && oMsgData['reload']) {
-            this.reload ();
-        }
-    }
-
-    drawerClose () {
-        this.setState ({drawerOpen: false});
-    }
-    
-    drawerOpen () {
-        this.setState ({drawerOpen: true});
-    }
-
-    onDrawerToggle () {
-        if ('undefined' === typeof(this.state.drawerOpen) || !this.state.drawerOpen)
-            this.setState ({drawerOpen: true});
-        else
-            this.setState ({drawerOpen: false});
-    }
-
-    render() {
-        var sUri = `${BASE_URL}?skin=${TEMPLATE}&mix=${'dark' === this.state.colorScheme ? MIX_DARK : MIX_LIGHT}`;
-        // if (this.state.data.loggedin && this.state.data.user_info && 'system' == this.state.data.user_info.module) // conditional redirect after login
-        //    sUri = `${BASE_URL}create-persons-profile`;
-
-        var sWebview = (
-                <WebView
-                    useWebKit={true}
-
-                    ref="webView1"
-                    key={this.state.key}
-                    geolocationEnabled={true}
-                    builtInZoomControls={false}
-                    decelerationRate="normal"
-                    cacheEnabled={true}
-                    allowsLinkPreview={true}
-                    hideKeyboardAccessoryView={true}
-                    injectedJavaScript={this.javascriptToInject()}
-                    onShouldStartLoadWithRequest={this.onWebViewShouldStartLoadWithRequest.bind(this)}
-                    onNavigationStateChange={this.onWebViewNavigationStateChange.bind(this)}
-                    style={styles.webview}
-                    source={{uri: sUri}}
-                    userAgent={"UNAMobileApp/Mobile (" + Platform.OS + ")"}
-                    onMessage={this.onWebViewMessage.bind(this)}
-                    allowFileAccess={true}
-                    onLoad={this.onWebViewLoadEnd.bind(this)}
-                    onLoadStart={this.onWebViewLoadStart.bind(this)}
-                    renderError={this.onWebViewRenderError.bind(this)}
-                    startInLoadingState={true}
-                    renderLoading={() => <View style={styles.webviewFirstLoad} bg={useTheme('colors.background')}><ActivityIndicator size="large" color={useTheme('colors.activityIndicator')} style={styles.webviewFirstLoadIndicator} /></View>}
-                />
-        );
-
-        return (
-            <NativeBaseProvider> 
-                <StatusBar animated={true} backgroundColor={useTheme('colors.statusBar')} barStyle={useTheme('barStyle')} />
-
-                {this.state.searchbar ? (
-                    <UnaToolbarSearch onSearch={this.onSearch} onSearchCancel={this.onSearchCancelMenu} />
-                ) : (
-                    <UnaToolbar drawerOpen={this.state.drawerOpen} loading={this.state.loading} loggedin={this.state.data.loggedin} backButtonEnabled={this.state.backButtonEnabled} onMainMenu={this.onMainMenu} onHomeMenu={this.onHomeMenu} onSearchMenu={this.onSearchMenu} onBackAndMainMenu={this.onBackAndMainMenu} onBack={this.onBack} onDrawerToggle={this.onDrawerToggle} title={this.state.status} />
-                ) }
-
-                {this.state.videoCall && this.state.videoCallUri ? (
-                    <View style={styles.containerVideoCall}><VideoCall onConferenceTerminated={this.onConferenceTerminated} onConferenceJoined={this.onConferenceJoined} onConferenceWillJoin={this.onConferenceWillJoin} conferenceUri={this.state.videoCallUri} audio={this.videoCallAudio} userInfo={this.state.data.user_info} /></View>
-                ) : (
-                    <View />
-                )}
-
-                {sWebview}
-
-                { this.state.drawerOpen ? <UnaDrawer onLogin={this.onDrawerLoginMenu.bind(this)} onJoin={this.onDrawerJoinMenu.bind(this)} onForotPassword={this.onDrawerForgotMenu.bind(this)} onClose={this.drawerClose.bind(this)} /> : <View /> }
-
-                {this.state.data.loggedin && (
-                    <UnaFooter bubblesNum={this.state.data.bubbles_num} bubbles={this.state.data.bubbles} onMainMenu={this.onMainMenu} onNotificationsMenu={this.onNotificationsMenu} onVideoCallToggle={this.onVideoCallToggle} onRequestPurchase={this.onRequestPurchase} onRequestSubscription={this.onRequestSubscription} onAddMenu={this.onAddMenu} onMessengerMenu={this.onMessengerMenu} onProfileMenu={this.onProfileMenu} />
-                )}
-            </NativeBaseProvider>
-        );
-    }
-}
-
-function UnaFooter(o) {
-
-    return (
-            <HStack bg={useTheme('colors.primary')} borderColor={useTheme('colors.toolbarBorder')} alignItems="center" safeAreaBottom style={styles.footer}>
-                {!('decorous' == TEMPLATE && Platform.isPad) &&
-                    (<Pressable py="3" flex={1} style={styles.footerTab}>
-                        <IconButton icon={<Icons.Apps size="xl" style={styles.footerIcon} color={useTheme('colors.textOnPrimary')} />} onPress={o.onMainMenu} />
-                    </Pressable>)
-                }
-                <Pressable py="3" flex={1} style={styles.footerTab}>
-                    <IconButton icon={<Icons.Bell size="xl"style={styles.footerIcon} color={useTheme('colors.textOnPrimary')} />} onPress={o.onNotificationsMenu} />
-                    {o.bubbles['notifications-preview'] > 0 && (<Badge num={o.bubbles['notifications-preview']} />)}
-                </Pressable>
-{/*
-                <Pressable py="3" flex={1} style={styles.footerTab}>
-                    <Center><IconButton icon={<Icons.Video size="xl" style={styles.footerIcon} color={useTheme('colors.textOnPrimary')} />} onPress={o.onVideoCallToggle} /></Center>
-                </Pressable>
-*/}
-                <Pressable py="3" flex={1} style={styles.footerTab}>
-                    <IconButton icon={<Icons.Plus size="xl" style={styles.footerIcon} color={useTheme('colors.textOnPrimary')} />} onPress={o.onAddMenu} />
-                </Pressable>
-                <Pressable py="3" flex={1} style={styles.footerTab}>                    
-                    <IconButton icon={<Icons.Chat size="xl" style={styles.footerIcon} color={useTheme('colors.textOnPrimary')} />} onPress={o.onMessengerMenu} />
-                    {o.bubbles['notifications-messenger'] > 0 && (<Badge num={o.bubbles['notifications-messenger']} />)}
-                </Pressable>
-                <Pressable py="3" flex={1} style={styles.footerTab}>
-                    <IconButton icon={<Icons.User size="xl" style={styles.footerIcon} color={useTheme('colors.textOnPrimary')} />} onPress={o.onProfileMenu} />
-                    {o.bubbles['account'] > 0 && (<Badge num={o.bubbles['account']} />)}
-                </Pressable>
-            </HStack>
-    );
-}
-
-function UnaToolbar(o) {
-
-    return (
-    <View style={{borderBottomColor: useTheme('colors.toolbarBorder'), borderBottomWidth:0.5}}>
-        <Box safeAreaTop bg={useTheme('colors.statusBar')} />
-        <HStack bg={useTheme('colors.primary')} px="1" py="1" justifyContent="space-between" alignItems="center" w="100%" style={styles.header}>
-            <HStack alignItems="center">
-
-                {o.loading ? (
-                    <ActivityIndicator size="small" color={useTheme('colors.activityIndicator')} style={styles.loadingIndicator} />
-                ):(
-                    o.loggedin ? (
-                        <IconButton icon={<Icons.Back size="lg" color={o.backButtonEnabled ? useTheme('colors.textOnPrimary') : useTheme('colors.textOnPrimaryDisabled')} style={styles.headerIcon} />} onPress={o.onBack} />
-                    ):(
-                        <IconButton icon={o.drawerOpen ? (<Icons.Cross size="lg" color={useTheme('colors.textOnPrimary')} style={styles.headerIcon} />) : (<Icons.Bars size="lg" color={useTheme('colors.textOnPrimary')} style={styles.headerIcon} />)} onPress={o.onDrawerToggle} />
-                    )
-                )}
-                
-                {!o.title || TITLE == o.title || o.loading ? (<View>
-                    {/*<Image alt="Logo" style={styles.headerImage} source={require('./img/logo.png')} />*/}
-                    <Icons.Logo size={75} />
-                </View>) : (
-                    <Text color={useTheme('colors.textOnPrimary')} numberOfLines={1} ellipsizeMode="tail" style={styles.headerTitle}>{o.title}</Text>
-                )}
-
-
-            </HStack>
-
-            <HStack>
-                {o.loggedin && (<IconButton icon={<Icons.Search size="lg" color={useTheme('colors.textOnPrimary')} style={styles.headerIcon} />} onPress={o.onSearchMenu} />)}
-            </HStack>
-
-        </HStack>
-    </View>
-    );
-}
-
-function UnaToolbarSearch(o) {
-    return (
-    <View style={{borderBottomColor: useTheme('colors.toolbarBorder'), borderBottomWidth:0.5}}>
-        <StatusBar bg={useTheme('colors.statusBar')} barStyle={useTheme('barStyle')} />
-        <Box safeAreaTop bg={useTheme('colors.statusBar')} />
-        <HStack bg={useTheme('colors.primary')} px="1" py="1" justifyContent="space-between" alignItems="center" w="100%" style={styles.header}>
-            <HStack alignItems="center" style={styles.searchInputContainer}>
-                <Input placeholder="Search..." width="89%" borderRadius={'ios' === Platform.OS ? 'full' : 'md'} px="3" fontSize="14" bg={useTheme('searchInputBackground')} color={useTheme('colors.searchInputText')} borderColor={useTheme('colors.searchInputBorder')} onSubmitEditing={o.onSearch} _focus={{ borderColor: useTheme('colors.searchInputBorderActive') }} placeholderTextColor={useTheme('colors.searchInputTextPlaceholder')} />
-                <IconButton icon={<Icons.Cross size="sm" color={useTheme('colors.textOnPrimary')} />} onPress={o.onSearchCancel} />
-            </HStack>
-        </HStack>
-    </View>
-    );
-}
-
-function UnaDrawer(o) {
-    const {StatusBarManager} = NativeModules;
-    const height = StatusBarManager.HEIGHT;
-    return (
-        <Box bg={useTheme('colors.drawerBackground')} borderColor={useTheme('colors.drawerBorder')} style={[styles.drawerContainer, {top:height + ('ios' === Platform.OS ? 50 : 25)}]} shadow={2}>
-            <VStack space={2}>
-                <Pressable style={styles.drawerButton} iconLeft transparent onPress={o.onLogin}>
-                    <HStack>
-                        <Icons.In size="md" color={useTheme('colors.drawerText')} style={styles.drawerButtonIcon} />
-                        <Text fontSize="md" color={useTheme('colors.drawerText')} style={styles.drawerButtonText}>Login</Text>
-                    </HStack>
-                </Pressable>
-                <Pressable style={styles.drawerButton} iconLeft transparent onPress={o.onJoin}>
-                    <HStack>
-                        <Icons.PlusCircle size="md" color={useTheme('colors.drawerText')} style={styles.drawerButtonIcon} />
-                        <Text fontSize="md" color={useTheme('colors.drawerText')} style={styles.drawerButtonText}>Join</Text>
-                    </HStack>
-                </Pressable>
-                <Pressable style={styles.drawerButton} iconLeft transparent onPress={o.onForotPassword}>
-                    <HStack>
-                        <Icons.Question size="md" color={useTheme('colors.drawerText')} style={styles.drawerButtonIcon} />
-                        <Text fontSize="md" color={useTheme('colors.drawerText')} style={styles.drawerButtonText}>Forgot Password</Text>
-                    </HStack>
-                </Pressable>
-            </VStack>
-        </Box>
-  );
-}
-
-function Badge(o) {
-    return (
-        <Box style={styles.badge}><Text style={styles.badgeText}>{o.num}</Text></Box>
-    );
-}
-
-const styles = new StyleSheet.create({
-    webview: {
-        flex: 1,
-        backgroundColor: 'transparent',
-    },
-    webviewFirstLoad: {
-        flex: 9999999999,        
-        justifyContent: 'center',  
-        alignItems: 'center',
-    },
-
-    header: {
-        height: 50,
-    },
-    headerTitle: {
-        fontSize: 18,
-        fontWeight: 'bold',
-    },
-    headerImage: {
-        width: 99,
-        height: 28,
-    },
-
-    searchInputContainer: {
-        paddingLeft:5,
-    },
-    searchInputItem: {        
-        borderWidth: 1,
-    },
-
-    footer: {
-        borderTopWidth: 0.5,        
-    },
-
-    containerVideoCall: {
-        backgroundColor: '#000',
-        flex: 9999999999,
-    },
-
-    loadingIndicator: {
-        width:44,
-        height:44,
-    },
-
-    drawerContainer: {
-        position: 'absolute',
-        top:0,
-        left:0,
-        width:250,        
-        borderWidth: 0.5,
-        padding:20,
-    },
-    drawerButton: {
-        justifyContent: 'flex-start',
-    },
-    drawerButtonIcon: {
-        top:3,
-        marginRight:10,
-        justifyContent: 'center',
-        textAlign: 'center',
-    },
-
-    badge: {
-        backgroundColor:'red', 
-        color:'white',
-        position:'absolute',
-        top:4,
-        right:11,
-        paddingLeft:4,
-        paddingRight:4,
-        borderRadius:3
-    },
-    badgeText: {
-        color:'white', 
-        fontSize: 11, 
-        fontWeight: 'bold',
-    },
-});

+ 0 - 118
App.tsx

@@ -1,118 +0,0 @@
-/**
- * Sample React Native App
- * https://github.com/facebook/react-native
- *
- * @format
- */
-
-import React from 'react';
-import type {PropsWithChildren} from 'react';
-import {
-  SafeAreaView,
-  ScrollView,
-  StatusBar,
-  StyleSheet,
-  Text,
-  useColorScheme,
-  View,
-} from 'react-native';
-
-import {
-  Colors,
-  DebugInstructions,
-  Header,
-  LearnMoreLinks,
-  ReloadInstructions,
-} from 'react-native/Libraries/NewAppScreen';
-
-type SectionProps = PropsWithChildren<{
-  title: string;
-}>;
-
-function Section({children, title}: SectionProps): JSX.Element {
-  const isDarkMode = useColorScheme() === 'dark';
-  return (
-    <View style={styles.sectionContainer}>
-      <Text
-        style={[
-          styles.sectionTitle,
-          {
-            color: isDarkMode ? Colors.white : Colors.black,
-          },
-        ]}>
-        {title}
-      </Text>
-      <Text
-        style={[
-          styles.sectionDescription,
-          {
-            color: isDarkMode ? Colors.light : Colors.dark,
-          },
-        ]}>
-        {children}
-      </Text>
-    </View>
-  );
-}
-
-function App(): JSX.Element {
-  const isDarkMode = useColorScheme() === 'dark';
-
-  const backgroundStyle = {
-    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
-  };
-
-  return (
-    <SafeAreaView style={backgroundStyle}>
-      <StatusBar
-        barStyle={isDarkMode ? 'light-content' : 'dark-content'}
-        backgroundColor={backgroundStyle.backgroundColor}
-      />
-      <ScrollView
-        contentInsetAdjustmentBehavior="automatic"
-        style={backgroundStyle}>
-        <Header />
-        <View
-          style={{
-            backgroundColor: isDarkMode ? Colors.black : Colors.white,
-          }}>
-          <Section title="Step One">
-            Edit <Text style={styles.highlight}>App.tsx</Text> to change this
-            screen and then come back to see your edits.
-          </Section>
-          <Section title="See Your Changes">
-            <ReloadInstructions />
-          </Section>
-          <Section title="Debug">
-            <DebugInstructions />
-          </Section>
-          <Section title="Learn More">
-            Read the docs to discover what to do next:
-          </Section>
-          <LearnMoreLinks />
-        </View>
-      </ScrollView>
-    </SafeAreaView>
-  );
-}
-
-const styles = StyleSheet.create({
-  sectionContainer: {
-    marginTop: 32,
-    paddingHorizontal: 24,
-  },
-  sectionTitle: {
-    fontSize: 24,
-    fontWeight: '600',
-  },
-  sectionDescription: {
-    marginTop: 8,
-    fontSize: 18,
-    fontWeight: '400',
-  },
-  highlight: {
-    fontWeight: '700',
-  },
-});
-
-export default App;

+ 0 - 46
ErrorPage.js

@@ -1,46 +0,0 @@
-
-import React, { Component } from 'react';
-import { StyleSheet } from 'react-native';
-import { View, Container, Text, Icon, Button } from 'native-base';
-import { themeDark, themeLight, useTheme } from './Theme';
-
-class ErrorPage extends React.Component {
-    constructor(props) {
-        super(props);
-    }
-    render() {
-        return (
-            <View style={styles.container}>
-                <Icon style={styles.icon} name="frown" type="FontAwesome5" />
-                <Text style={styles.text}>Ops, something went wrong...</Text>
-                <View style={styles.buttonContainer}>
-                    <Button onPress={this.props.onReload}><Text>Retry</Text></Button>
-                </View>
-            </View>
-        );
-    }
-}
-
-const styles = StyleSheet.create({
-    container: {
-        alignItems: 'center', 
-        justifyContent: 'center', 
-        flex: 9999999999,
-        backgroundColor: useTheme('colors.background'),
-    },
-    icon: {
-        fontSize: 100, 
-        color: useTheme('colors.text'),
-    },
-    text: {
-        color: useTheme('colors.text'),
-        marginTop: 20, 
-        marginBottom: 20,
-    },
-    buttonContainer: {
-        justifyContent: 'center', 
-        alignItems: 'center'
-    }
-});
-
-export default ErrorPage;

+ 0 - 6
Gemfile

@@ -1,6 +0,0 @@
-source 'https://rubygems.org'
-
-# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
-ruby '>= 2.6.10'
-
-gem 'cocoapods', '>= 1.11.3'

+ 0 - 98
Gemfile.lock

@@ -1,98 +0,0 @@
-GEM
-  remote: https://rubygems.org/
-  specs:
-    CFPropertyList (3.0.6)
-      rexml
-    activesupport (7.0.4.3)
-      concurrent-ruby (~> 1.0, >= 1.0.2)
-      i18n (>= 1.6, < 2)
-      minitest (>= 5.1)
-      tzinfo (~> 2.0)
-    addressable (2.8.4)
-      public_suffix (>= 2.0.2, < 6.0)
-    algoliasearch (1.27.5)
-      httpclient (~> 2.8, >= 2.8.3)
-      json (>= 1.5.1)
-    atomos (0.1.3)
-    claide (1.1.0)
-    cocoapods (1.12.1)
-      addressable (~> 2.8)
-      claide (>= 1.0.2, < 2.0)
-      cocoapods-core (= 1.12.1)
-      cocoapods-deintegrate (>= 1.0.3, < 2.0)
-      cocoapods-downloader (>= 1.6.0, < 2.0)
-      cocoapods-plugins (>= 1.0.0, < 2.0)
-      cocoapods-search (>= 1.0.0, < 2.0)
-      cocoapods-trunk (>= 1.6.0, < 2.0)
-      cocoapods-try (>= 1.1.0, < 2.0)
-      colored2 (~> 3.1)
-      escape (~> 0.0.4)
-      fourflusher (>= 2.3.0, < 3.0)
-      gh_inspector (~> 1.0)
-      molinillo (~> 0.8.0)
-      nap (~> 1.0)
-      ruby-macho (>= 2.3.0, < 3.0)
-      xcodeproj (>= 1.21.0, < 2.0)
-    cocoapods-core (1.12.1)
-      activesupport (>= 5.0, < 8)
-      addressable (~> 2.8)
-      algoliasearch (~> 1.0)
-      concurrent-ruby (~> 1.1)
-      fuzzy_match (~> 2.0.4)
-      nap (~> 1.0)
-      netrc (~> 0.11)
-      public_suffix (~> 4.0)
-      typhoeus (~> 1.0)
-    cocoapods-deintegrate (1.0.5)
-    cocoapods-downloader (1.6.3)
-    cocoapods-plugins (1.0.0)
-      nap
-    cocoapods-search (1.0.1)
-    cocoapods-trunk (1.6.0)
-      nap (>= 0.8, < 2.0)
-      netrc (~> 0.11)
-    cocoapods-try (1.2.0)
-    colored2 (3.1.2)
-    concurrent-ruby (1.2.2)
-    escape (0.0.4)
-    ethon (0.16.0)
-      ffi (>= 1.15.0)
-    ffi (1.15.5)
-    fourflusher (2.3.1)
-    fuzzy_match (2.0.4)
-    gh_inspector (1.1.3)
-    httpclient (2.8.3)
-    i18n (1.13.0)
-      concurrent-ruby (~> 1.0)
-    json (2.6.3)
-    minitest (5.18.0)
-    molinillo (0.8.0)
-    nanaimo (0.3.0)
-    nap (1.1.0)
-    netrc (0.11.0)
-    public_suffix (4.0.7)
-    rexml (3.2.5)
-    ruby-macho (2.5.1)
-    typhoeus (1.4.0)
-      ethon (>= 0.9.0)
-    tzinfo (2.0.6)
-      concurrent-ruby (~> 1.0)
-    xcodeproj (1.22.0)
-      CFPropertyList (>= 2.3.3, < 4.0)
-      atomos (~> 0.1.3)
-      claide (>= 1.0.2, < 2.0)
-      colored2 (~> 3.1)
-      nanaimo (~> 0.3.0)
-      rexml (~> 3.2.4)
-
-PLATFORMS
-  ruby
-
-DEPENDENCIES
-  cocoapods (>= 1.11.3)
-
-RUBY VERSION
-   ruby 2.7.5p203
-
-BUNDLED WITH
-   2.1.4

+ 0 - 263
Icons.js

@@ -1,263 +0,0 @@
-
-import * as React from 'react';
-import { Icon } from 'native-base';
-import Svg, {
-  Defs,
-  ClipPath,
-  Rect,
-  G,
-  LinearGradient,
-  Stop,
-  Path,
-  RadialGradient,
-} from "react-native-svg";
-
-const Logo = (o) => (
-  <Icon size={o.size} viewBox="0 0 3264 1024" style={o.style}>
-    <Path
-      d="M840.961 925.025c-10.369 8.269-10.635 24.061.983 30.457 33.884 18.652 80.294 30.953 126.4 35.03 11.27.996 20.659-8.109 20.751-19.423.18-21.96.59-48.97 1.047-79.089.425-27.979.891-58.641 1.25-90.429.182-16.128-23.798-22.051-32.398-8.407-31.718 50.318-71.697 94.907-118.033 131.861Z"
-      fill="url(#a)"
-    />
-    <Path
-      d="M181.2 100.447c10.709-8.618 10.49-25.144-1.743-31.413-36.658-18.789-83.834-31.207-126.784-35.415C41.4 32.514 32 41.623 32 52.949v170.23c0 16.097 24.036 21.932 32.6 8.302 31.362-49.915 70.847-94.211 116.6-131.034Z"
-      fill="url(#b)"
-    />
-    <Path
-      d="M512 992c265.097 0 480-214.903 480-480 0-135.453-56.107-257.802-146.347-345.074C827.875 149.733 800 163.288 800 188.019V512c0 159.058-128.942 288-288 288-76.388 0-145.831-29.74-197.38-78.273-14.417-13.573-42.62-4.071-42.62 15.73v167.256c0 14.279 7.552 27.593 20.241 34.139C358.087 972.82 432.804 992 512 992Z"
-      fill="url(#c)"
-    />
-    <Path
-      d="M709.38 302.273c14.417 13.573 42.62 4.071 42.62-15.73V119.287c0-14.279-7.552-27.593-20.242-34.139C665.913 51.18 591.196 32 512 32 246.903 32 32 246.903 32 512c0 135.453 56.106 257.802 146.347 345.074C196.125 874.267 224 860.712 224 835.981V512c0-159.058 128.942-288 288-288 76.388 0 145.831 29.74 197.38 78.273Z"
-      fill="url(#d)"
-    />
-    <Path
-      fillRule="evenodd"
-      clipRule="evenodd"
-      d="M1706.33 771.031c-58.89 34.212-111.66 34.212-140.16 34.212-.12 0-.25 0-.38-.003-.12.003-.25.003-.38.003-28.49 0-81.27 0-140.15-34.212-25.88-13.977-49.97-32.239-72.28-54.786-59.32-58.71-88.98-129.783-88.98-213.216V222.21c0-10.274 7.81-18.787 18.08-18.416 26.53.959 75.45 8.453 99.23 48.534 2.97 4.995 4.12 10.828 4.12 16.636v234.065c0 55.242 17.6 92.702 52.83 127.932 35.13 35.137 77.64 52.75 127.53 52.839 49.89-.089 92.4-17.702 127.54-52.839 35.22-35.23 47-70.882 52.83-127.932 5.26-53.878 5.69-126.97 1.11-221.449-.42-8.485 9.23-13.513 15.7-8.008 118.87 101.119 104.61 229.457 104.61 229.457 0 83.433-29.66 154.506-88.98 213.216-22.31 22.547-46.4 40.809-72.27 54.786Z"
-      fill="url(#e)"
-    />
-    <Path
-      fillRule="evenodd"
-      clipRule="evenodd"
-      d="M2123.11 242.93c58.88-34.211 111.66-34.211 140.15-34.211h.76c28.5 0 81.27 0 140.16 34.211 25.87 13.978 49.96 32.239 72.27 54.784 59.33 58.711 88.99 129.784 88.99 213.216v280.82c0 10.273-7.81 18.786-18.08 18.415-26.54-.959-75.46-8.451-99.24-48.532-2.96-4.995-4.11-10.829-4.11-16.636V510.93c0-59.212-17.61-92.702-52.83-127.931-35.14-35.137-77.65-52.75-127.54-52.839-49.89.089-92.4 17.702-127.53 52.839-35.22 35.229-52.83 67.846-52.83 127.931v222.878c0 8.242-9.4 12.833-15.7 7.52-120.07-101.261-105.73-230.398-105.73-230.398 0-83.432 29.66-154.505 88.99-213.216 22.3-22.545 46.39-40.806 72.27-54.784Z"
-      fill="url(#f)"
-    />
-    <Path
-      fillRule="evenodd"
-      clipRule="evenodd"
-      d="M3263.25 501.331c.4 3.024.63 6.222.63 9.601v280.819c0 10.273-7.81 18.786-18.08 18.415-28.18-1.019-81.6-9.406-103.35-56.354.03-.681.06-1.361.11-2.041-50.37 37.764-112.94 60.137-180.74 60.137-166.54 0-301.54-135.029-301.54-301.596 0-166.565 135-301.593 301.54-301.593 163.54 0 296.69 130.201 301.43 292.612ZM2961.82 689.95c99.2 0 179.63-80.425 179.63-179.638 0-99.211-80.43-179.637-179.63-179.637-99.17 0-179.6 80.426-179.6 179.637 0 99.213 80.43 179.638 179.6 179.638Z"
-      fill="url(#g)"
-    />
-    <Defs>
-      <RadialGradient
-        id="a"
-        cx={0}
-        cy={0}
-        r={1}
-        gradientUnits="userSpaceOnUse"
-        gradientTransform="matrix(0 260 -165.059 0 992 732)"
-      >
-        <Stop stopColor="#FB923C" />
-        <Stop offset={1} stopColor="#EF4444" />
-      </RadialGradient>
-      <RadialGradient
-        id="b"
-        cx={0}
-        cy={0}
-        r={1}
-        gradientUnits="userSpaceOnUse"
-        gradientTransform="matrix(0 -260 171.858 0 32 291.999)"
-      >
-        <Stop stopColor="#0EA5E9" />
-        <Stop offset={1} stopColor="#38BDF8" />
-      </RadialGradient>
-      <RadialGradient
-        id="c"
-        cx={0}
-        cy={0}
-        r={1}
-        gradientUnits="userSpaceOnUse"
-        gradientTransform="matrix(0 864 -719.971 0 632 128)"
-      >
-        <Stop stopColor="#38BDF8" />
-        <Stop offset={1} stopColor="#0284C7" />
-      </RadialGradient>
-      <RadialGradient
-        id="d"
-        cx={0}
-        cy={0}
-        r={1}
-        gradientUnits="userSpaceOnUse"
-        gradientTransform="matrix(0 864 -719.971 0 525.5 32)"
-      >
-        <Stop stopColor="#34D399" />
-        <Stop offset={1} stopColor="#059669" />
-      </RadialGradient>
-      <LinearGradient
-        id="e"
-        x1={1566.06}
-        y1={203.675}
-        x2={1566.06}
-        y2={805.243}
-        gradientUnits="userSpaceOnUse"
-      >
-        <Stop stopColor="#F59E0B" />
-        <Stop offset={1} stopColor="#F97316" />
-      </LinearGradient>
-      <LinearGradient
-        id="f"
-        x1={2263.38}
-        y1={208.719}
-        x2={2263.38}
-        y2={810.284}
-        gradientUnits="userSpaceOnUse"
-      >
-        <Stop stopColor="#F59E0B" />
-        <Stop offset={1} stopColor="#F97316" />
-      </LinearGradient>
-      <LinearGradient
-        id="g"
-        x1={2962.08}
-        y1={208.719}
-        x2={2962.08}
-        y2={811.908}
-        gradientUnits="userSpaceOnUse"
-      >
-        <Stop stopColor="#F59E0B" />
-        <Stop offset={1} stopColor="#F97316" />
-      </LinearGradient>
-    </Defs>
-  </Icon>
-);
-
-function Home(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" stroke-width="1.5" stroke={o.color} class="w-6 h-6">
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
-            </G>
-        </Icon>
-    );
-}
-
-function Apps(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" stroke-width="1.5" stroke={o.color} class="w-6 h-6">
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
-            </G>
-        </Icon>
-    );
-}
-
-function Bell(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" stroke-width="1.5" stroke={o.color} class="w-6 h-6">
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
-            </G>
-        </Icon>
-    );
-}
-
-function Plus(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" stroke-width="1.5" stroke={o.color} class="w-6 h-6">
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
-            </G>
-        </Icon>
-    );
-}
-
-function Chat(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" stroke-width="1.5" stroke={o.color} class="w-6 h-6">
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M8.625 9.75a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375m-13.5 3.01c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 01.778-.332 48.294 48.294 0 005.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
-            </G>
-        </Icon>
-    );
-}
-
-function User(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" stroke-width="1.5" stroke={o.color} class="w-6 h-6">
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
-            </G>
-        </Icon>
-    );
-}
-
-function Bars(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" strokeWidth={1.5} stroke={o.color} className="w-6 h-6">
-                <Path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
-            </G>
-        </Icon>
-    );
-}
-
-function Search(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" stroke-width="1.5" stroke={o.color} class="w-6 h-6">
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
-            </G>
-        </Icon>
-    );
-}
-
-function Back(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" stroke-width="1.5" stroke={o.color} class="w-6 h-6">
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
-            </G>
-        </Icon>
-    );
-}
-
-function Cross(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" stroke-width="1.5" stroke={o.color} class="w-6 h-6">  
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
-            </G>
-        </Icon>
-    );
-}
-
-function In(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" strokeWidth={1.5} stroke={o.color} className="w-6 h-6">
-                <Path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
-            </G>
-        </Icon>
-    );
-}
-
-function PlusCircle(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" strokeWidth={1.5} stroke={o.color} className="w-6 h-6">
-                <Path strokeLinecap="round" strokeLinejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
-            </G>
-        </Icon>
-    );
-}
-
-function Question(o) {
-    return (
-        <Icon size={o.size} viewBox="0 0 24 24" style={o.style}>
-            <G fill="none" strokeWidth={1.5} stroke={o.color} className="w-6 h-6">
-                <Path strokeLinecap="round" strokeLinejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
-            </G>
-        </Icon>
-    );
-}
-
-export {Logo, Home, Apps, Bell, Plus, Chat, User, Bars, Search, Back, Cross, In, PlusCircle, Question};

+ 33 - 69
README.md

@@ -1,86 +1,50 @@
-# UNA Mobile Apps
+# Welcome to your Expo app 👋
 
-This is source code of UNA mobile apps for iOS and Android based on ReactNative.   
-Mobile apps need to have **Nexus** UNA app installed.
+This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
 
-## Run
+## Get started
 
-You need to have [Node.js](https://nodejs.org/), [Android SDK](https://developer.android.com/studio) for Android Apps and [XCode](https://developer.apple.com/xcode/) for iOS apps along withg [CocoaPods](https://cocoapods.org/) installed to build apps.
+1. Install dependencies
 
-After downloading source code, unpacking it, then run:
-```bash
-npm install 
-```
-Then you can already try to run Andoid app with the following commands (Android emulator must be already running):
-```bash
-export ANDROID_HOME=/path/to/android/sdk
-npx react-native run-android
-```
-or iOS app (on Mac OSX only):
-```
-cd ios; pod install; cd ..
-npx react-native run-ios
-```
+   ```bash
+   npm install
+   ```
 
-## Changing display name and bundle identifier
+2. Start the app
 
-There is `rebrand.sh` script to help with below changes, just change variables in the beginning of the file and run it, or proceed with the manual actions below:
+   ```bash
+   npx expo start
+   ```
 
-Lest assume that you want to rename app to **Kookaburra** and site name is **kookaburra.io**.   
-Then you need to change the following strings:
-```
-UNA.IO => Kookaburra
-com.una.android => com.kookaburra.android
-com.una.ios => com.kookaburra.ios
-una.io => kookaburra.io
-una => kookaburra
-```
+In the output, you'll find options to open the app in a
 
-In the following files and some files need to be renamed:
-```
-App.js
-app.json
-package.json
-
-ios/Podfile
-ios/una/AppDelegate.m
-
-android/app/src/main/res/values/strings.xml
-android/app/src/main/java/com/una/android/MainActivity.java => android/app/src/main/java/com/kookaburra/android/MainActivity.java
-android/app/src/main/java/com/una/android/MainApplication.java => android/app/src/main/java/com/kookaburra/android/MainApplication.java
-android/app/src/main/AndroidManifest.xml
-android/app/BUCK
-android/app/build.gradle
-```
+- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
+- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
+- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
+- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
 
-In XCode change the following (make sure to open `una.xcworkspace`):
+You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
 
-![](https://raw.githubusercontent.com/wiki/unaio/una/images/mobile-apps/change-name-ios.png)
+## Get a fresh project
 
-Then rename the following file:
-```
-ios/kookaburra.xcodeproj/xcshareddata/xcschemes/una.xcscheme => ios/kookaburra.xcodeproj/xcshareddata/xcschemes/kookaburra.xcscheme
+When you're ready, run:
+
+```bash
+npm run reset-project
 ```
 
-## Change images to your own
+This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
 
-Change all images to your own in `/img/` folder, leaving the same images dimensions.  
+## Learn more
 
-To change launcher and icons for iOS and Android apps it's recommended to use special script, it can be installed using the following command:
-```bash
-npm i -D @bam.tech/react-native-make
-```
+To learn more about developing your project with Expo, look at the following resources:
 
-Then you can generate app icons and splash for Android using the following command:
-```bash
-npx react-native set-icon --platform android --path ./img/icon-android.png
-npx react-native set-splash --platform android --path ./img/background.png --resize contain
-```
+- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
+- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
 
-Before generating splash and app icon of iOS app rename `ios/una` folder to `ios/kookaburra`:
-```bash
-npx react-native set-icon --platform ios --path ./img/icon.png
-npx react-native set-splash --platform ios --path ./img/background.png --resize contain
-```
-After images are generated rename it back `ios/kookaburra` to `ios/una`.
-Then open `kookaburra.xcworkspace` in XCode and add `SplashScreen.storyboard` file to the project to use it as splash.
+## Join the community
+
+Join our community of developers creating universal apps.
+
+- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
+- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.

+ 0 - 59
Theme.js

@@ -1,59 +0,0 @@
-
-import {Appearance} from 'react-native';
-
-const themeDark = {
-    dark: true,
-    barStyle: 'light-content',
-    colors: {
-        primary: '#1e293b',
-        textOnPrimary: '#e2e8f0',
-        textOnPrimaryDisabled: '#111',
-        background: '#000000',
-        toolbarBorder: '#475569',
-        drawerBackground: '#F3F4F6',
-        drawerText: '#1890ff',
-        drawerBorder: '#475569',
-        statusBar: '#000',
-        activityIndicator: '#e2e8f0',
-        searchInputBackground: '#000000',
-        searchInputBorder: '#475569',
-        searchInputBorderActive: '#475569',
-        searchInputText: '#e2e8f0',
-        searchInputTextPlaceholder: '#888',
-    }
-};
-const themeLight = {
-    dark: false,
-    barStyle: 'dark-content',
-    colors: {
-        primary: '#fff',
-        textOnPrimary: '#334155',
-        textOnPrimaryDisabled: '#eee',
-        background: '#F3F4F6',
-        toolbarBorder: '#cbd5e1',
-        drawerBackground: '#F3F4F6',
-        drawerText: '#1890ff',
-        drawerBorder: '#cbd5e1',
-        statusBar: '#eee',
-        activityIndicator: '#334155',
-        searchInputBackground: '#F7F9FC',
-        searchInputBorder: '#cbd5e1',
-        searchInputBorderActive: '#cbd5e1',
-        searchInputText: '#334155',
-        searchInputTextPlaceholder: '#888',
-    }
-};
-
-function useTheme (s) {
-    var colorScheme = Appearance.getColorScheme();
-    var a = s.split('.'),
-        ret = 'dark' === colorScheme ? themeDark : themeLight,
-        i = 0;        
-    for (; i < a.length ; i++) {
-        if ('undefined' !== typeof(ret[a[i]]))
-            ret = ret[a[i]];
-    }
-    return ret;
-}
-
-export {themeDark, themeLight, useTheme};

+ 0 - 34
VideoCall.js

@@ -1,34 +0,0 @@
-import React from 'react';
-import { View } from 'react-native';
-import JitsiMeet, { JitsiMeetView } from 'react-native-jitsi-meet';
-
-class VideoCall extends React.Component {
-    constructor(props) {
-        super(props);
-    }
-
-    componentDidMount() {
-        setTimeout(() => {
-            if (this.props.audio)
-                JitsiMeet.audioCall(this.props.conferenceUri, this.props.userInfo);
-            else
-                JitsiMeet.call(this.props.conferenceUri, this.props.userInfo);
-        }, 1000);
-    }
-
-    render() {
-        return (
-            <JitsiMeetView onConferenceTerminated={this.props.onConferenceTerminated} onConferenceJoined={this.props.onConferenceJoined} onConferenceWillJoin={this.props.onConferenceWillJoin} style={{ flex: 1, height: '100%', width: '100%' }} />
-        );
-    }
-}
-
-VideoCall.defaultProps = { 
-    onConferenceTerminated: () => {},
-    onConferenceJoined: () => {},
-    onConferenceWillJoin: () => {},
-    userInfo: false,
-    audio: false,
-};
-
-export default VideoCall;

+ 0 - 14
__tests__/App-test.tsx

@@ -1,14 +0,0 @@
-/**
- * @format
- */
-
-import 'react-native';
-import React from 'react';
-import App from '../App';
-
-// Note: test renderer must be required after react-native.
-import renderer from 'react-test-renderer';
-
-it('renders correctly', () => {
-  renderer.create(<App />);
-});

+ 80 - 76
android/app/build.gradle

@@ -1,22 +1,32 @@
 apply plugin: "com.android.application"
+apply plugin: "org.jetbrains.kotlin.android"
 apply plugin: "com.facebook.react"
 
-import com.android.build.OutputFile
+def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
 
 /**
  * This is the configuration block to customize your React Native Android app.
  * By default you don't need to apply any configuration, just uncomment the lines you need.
  */
 react {
+    entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
+    reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
+    hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
+    codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
+
+    enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
+    // Use Expo CLI to bundle the app, this ensures the Metro config
+    // works correctly with Expo projects.
+    cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
+    bundleCommand = "export:embed"
+
     /* Folders */
-    //   The root of your project, i.e. where "package.json" lives. Default is '..'
-    // root = file("../")
-    //   The folder where the react-native NPM package is. Default is ../node_modules/react-native
-    // reactNativeDir = file("../node_modules/react-native")
-    //   The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
-    // codegenDir = file("../node_modules/react-native-codegen")
-    //   The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
-    // cliFile = file("../node_modules/react-native/cli.js")
+     //   The root of your project, i.e. where "package.json" lives. Default is '../..'
+    // root = file("../../")
+    //   The folder where the react-native NPM package is. Default is ../../node_modules/react-native
+    // reactNativeDir = file("../../node_modules/react-native")
+    //   The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
+    // codegenDir = file("../../node_modules/@react-native/codegen")
 
     /* Variants */
     //   The list of variants to that are debuggable. For those we're going to
@@ -27,9 +37,7 @@ react {
     /* Bundling */
     //   A list containing the node command and its flags. Default is just 'node'.
     // nodeExecutableAndArgs = ["node"]
-    //
-    //   The command to run when bundling. By default is 'bundle'
-    // bundleCommand = "ram-bundle"
+
     //
     //   The path to the CLI configuration file. Default is empty.
     // bundleConfig = file(../rn-cli.config.js)
@@ -50,20 +58,15 @@ react {
     //
     //   The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
     // hermesFlags = ["-O", "-output-source-map"]
-}
 
-/**
- * Set this to true to create four separate APKs instead of one,
- * one for each native architecture. This is useful if you don't
- * use App Bundles (https://developer.android.com/guide/app-bundle/)
- * and want to have separate APKs to upload to the Play Store.
- */
-def enableSeparateBuildPerCPUArchitecture = false
+    /* Autolinking */
+    autolinkLibrariesWithApp()
+}
 
 /**
- * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
+ * Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
  */
-def enableProguardInReleaseBuilds = false
+def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
 
 /**
  * The preferred build flavor of JavaScriptCore (JSC)
@@ -76,39 +79,23 @@ def enableProguardInReleaseBuilds = false
  * give correct results when using with locales other than en-US. Note that
  * this variant is about 6MiB larger per architecture than default.
  */
-def jscFlavor = 'org.webkit:android-jsc:+'
-
-/**
- * Private function to get the list of Native Architectures you want to build.
- * This reads the value from reactNativeArchitectures in your gradle.properties
- * file and works together with the --active-arch-only flag of react-native run-android.
- */
-def reactNativeArchitectures() {
-    def value = project.getProperties().get("reactNativeArchitectures")
-    return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
-}
+def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
 
 android {
     ndkVersion rootProject.ext.ndkVersion
 
-    compileSdkVersion rootProject.ext.compileSdkVersion
+    buildToolsVersion rootProject.ext.buildToolsVersion
+    compileSdk rootProject.ext.compileSdkVersion
 
-    namespace "com.una.android"
+    namespace 'com.unacms.app'
     defaultConfig {
-        applicationId "com.una.android"
+        applicationId 'com.unacms.app'
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 2100
-        versionName "2.1.0"
-    }
+        versionCode 1
+        versionName "3.0.0"
 
-    splits {
-        abi {
-            reset()
-            enable enableSeparateBuildPerCPUArchitecture
-            universalApk false  // If true, also generate a universal APK
-            include (*reactNativeArchitectures())
-        }
+        buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
     }
     signingConfigs {
         debug {
@@ -117,14 +104,6 @@ android {
             keyAlias 'androiddebugkey'
             keyPassword 'android'
         }
-        release {
-            if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
-                storeFile file(MYAPP_RELEASE_STORE_FILE)
-                storePassword MYAPP_RELEASE_STORE_PASSWORD
-                keyAlias MYAPP_RELEASE_KEY_ALIAS
-                keyPassword MYAPP_RELEASE_KEY_PASSWORD
-            }
-        }
     }
     buildTypes {
         debug {
@@ -133,25 +112,42 @@ android {
         release {
             // Caution! In production, you need to generate your own keystore file.
             // see https://reactnative.dev/docs/signed-apk-android.
-            signingConfig signingConfigs.release
-            minifyEnabled enableProguardInReleaseBuilds
+            signingConfig signingConfigs.debug
+            def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
+            shrinkResources enableShrinkResources.toBoolean()
+            minifyEnabled enableMinifyInReleaseBuilds
             proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
+            def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
+            crunchPngs enablePngCrunchInRelease.toBoolean()
         }
     }
+    packagingOptions {
+        jniLibs {
+            def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
+            useLegacyPackaging enableLegacyPackaging.toBoolean()
+        }
+    }
+    androidResources {
+        ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
+    }
+}
 
-    // applicationVariants are e.g. debug, release
-    applicationVariants.all { variant ->
-        variant.outputs.each { output ->
-            // For each separate APK per architecture, set a unique version code as described here:
-            // https://developer.android.com/studio/build/configure-apk-splits.html
-            // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
-            def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
-            def abi = output.getFilter(OutputFile.ABI)
-            if (abi != null) {  // null for the universal-debug, universal-release variants
-                output.versionCodeOverride =
-                        defaultConfig.versionCode * 1000 + versionCodes.get(abi)
-            }
-
+// Apply static values from `gradle.properties` to the `android.packagingOptions`
+// Accepts values in comma delimited lists, example:
+// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
+["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
+    // Split option: 'foo,bar' -> ['foo', 'bar']
+    def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
+    // Trim all elements in place.
+    for (i in 0..<options.size()) options[i] = options[i].trim();
+    // `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
+    options -= ""
+
+    if (options.length > 0) {
+        println "android.packagingOptions.$prop += $options ($options.length)"
+        // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
+        options.each {
+            android.packagingOptions[prop] += it
         }
     }
 }
@@ -160,19 +156,27 @@ dependencies {
     // The version of react-native is set by the React Native Gradle Plugin
     implementation("com.facebook.react:react-android")
 
-    implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
+    def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
+    def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
+    def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
+
+    if (isGifEnabled) {
+        // For animated gif support
+        implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
+    }
 
-    debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
-    debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
-        exclude group:'com.squareup.okhttp3', module:'okhttp'
+    if (isWebpEnabled) {
+        // For webp support
+        implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
+        if (isWebpAnimatedEnabled) {
+            // Animated webp support
+            implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
+        }
     }
 
-    debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
     if (hermesEnabled.toBoolean()) {
         implementation("com.facebook.react:hermes-android")
     } else {
         implementation jscFlavor
     }
 }
-
-apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

+ 4 - 0
android/app/proguard-rules.pro

@@ -7,4 +7,8 @@
 # For more details, see
 #   http://developer.android.com/guide/developing/tools/proguard.html
 
+# react-native-reanimated
+-keep class com.swmansion.reanimated.** { *; }
+-keep class com.facebook.react.turbomodule.** { *; }
+
 # Add any project specific keep options here:

+ 1 - 7
android/app/src/debug/AndroidManifest.xml

@@ -1,13 +1,7 @@
-<?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
 
     <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
 
-    <application
-        android:usesCleartextTraffic="true"
-        tools:targetApi="28"
-        tools:ignore="GoogleAppIndexingWarning">
-        <activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false" />
-    </application>
+    <application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
 </manifest>

+ 0 - 75
android/app/src/debug/java/com/una/ReactNativeFlipper.java

@@ -1,75 +0,0 @@
-/**
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * <p>This source code is licensed under the MIT license found in the LICENSE file in the root
- * directory of this source tree.
- */
-package com.una.android;
-
-import android.content.Context;
-import com.facebook.flipper.android.AndroidFlipperClient;
-import com.facebook.flipper.android.utils.FlipperUtils;
-import com.facebook.flipper.core.FlipperClient;
-import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
-import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
-import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
-import com.facebook.flipper.plugins.inspector.DescriptorMapping;
-import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
-import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
-import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
-import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
-import com.facebook.react.ReactInstanceEventListener;
-import com.facebook.react.ReactInstanceManager;
-import com.facebook.react.bridge.ReactContext;
-import com.facebook.react.modules.network.NetworkingModule;
-import okhttp3.OkHttpClient;
-
-/**
- * Class responsible of loading Flipper inside your React Native application. This is the debug
- * flavor of it. Here you can add your own plugins and customize the Flipper setup.
- */
-public class ReactNativeFlipper {
-  public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
-    if (FlipperUtils.shouldEnableFlipper(context)) {
-      final FlipperClient client = AndroidFlipperClient.getInstance(context);
-
-      client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
-      client.addPlugin(new DatabasesFlipperPlugin(context));
-      client.addPlugin(new SharedPreferencesFlipperPlugin(context));
-      client.addPlugin(CrashReporterPlugin.getInstance());
-
-      NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
-      NetworkingModule.setCustomClientBuilder(
-          new NetworkingModule.CustomClientBuilder() {
-            @Override
-            public void apply(OkHttpClient.Builder builder) {
-              builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
-            }
-          });
-      client.addPlugin(networkFlipperPlugin);
-      client.start();
-
-      // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
-      // Hence we run if after all native modules have been initialized
-      ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
-      if (reactContext == null) {
-        reactInstanceManager.addReactInstanceEventListener(
-            new ReactInstanceEventListener() {
-              @Override
-              public void onReactContextInitialized(ReactContext reactContext) {
-                reactInstanceManager.removeReactInstanceEventListener(this);
-                reactContext.runOnNativeModulesQueueThread(
-                    new Runnable() {
-                      @Override
-                      public void run() {
-                        client.addPlugin(new FrescoFlipperPlugin());
-                      }
-                    });
-              }
-            });
-      } else {
-        client.addPlugin(new FrescoFlipperPlugin());
-      }
-    }
-  }
-}

+ 31 - 37
android/app/src/main/AndroidManifest.xml

@@ -1,37 +1,31 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"  package="com.una.android">
-
-    <uses-permission android:name="android.permission.INTERNET" />
-
-    <application
-      android:name=".MainApplication"
-      android:label="@string/app_name"
-      android:icon="@mipmap/ic_launcher"
-      android:allowBackup="false"
-      android:theme="@style/AppTheme">
-      <activity
-        android:name=".MainActivity"
-        android:label="@string/app_name"
-        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
-        android:launchMode="singleTask"
-        android:windowSoftInputMode="adjustResize"
-        android:exported="true">
-        <intent-filter>
-            <action android:name="android.intent.action.MAIN" />
-            <category android:name="android.intent.category.LAUNCHER" />
-        </intent-filter>
-        <intent-filter android:label="UNA.IO">
-                <action android:name="android.intent.action.VIEW" />
-
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-
-                <data
-                    android:scheme="https"
-                    android:host="una.io" />
-                <data
-                    android:scheme="http"
-                    android:host="una.io" />
-        </intent-filter>
-      </activity>
-    </application>
-</manifest>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+  <uses-permission android:name="android.permission.INTERNET"/>
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+  <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+  <uses-permission android:name="android.permission.VIBRATE"/>
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+  <queries>
+    <intent>
+      <action android:name="android.intent.action.VIEW"/>
+      <category android:name="android.intent.category.BROWSABLE"/>
+      <data android:scheme="https"/>
+    </intent>
+  </queries>
+  <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
+    <meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
+    <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
+    <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
+    <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN"/>
+        <category android:name="android.intent.category.LAUNCHER"/>
+      </intent-filter>
+      <intent-filter>
+        <action android:name="android.intent.action.VIEW"/>
+        <category android:name="android.intent.category.DEFAULT"/>
+        <category android:name="android.intent.category.BROWSABLE"/>
+        <data android:scheme="unacms"/>
+      </intent-filter>
+    </activity>
+  </application>
+</manifest>

+ 0 - 41
android/app/src/main/java/com/una/android/MainActivity.java

@@ -1,41 +0,0 @@
-package com.una.android;
-
-import android.os.Bundle;
-import org.devio.rn.splashscreen.SplashScreen;
-import com.facebook.react.ReactActivity;
-import com.facebook.react.ReactActivityDelegate;
-import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
-import com.facebook.react.defaults.DefaultReactActivityDelegate;
-
-public class MainActivity extends ReactActivity {
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        SplashScreen.show(this, R.style.SplashScreenTheme, true);
-        super.onCreate(savedInstanceState);
-    }
-  /**
-   * Returns the name of the main component registered from JavaScript. This is used to schedule
-   * rendering of the component.
-   */
-  @Override
-  protected String getMainComponentName() {
-    return "una";
-  }
-
-  /**
-   * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
-   * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
-   * (aka React 18) with two boolean flags.
-   */
-  @Override
-  protected ReactActivityDelegate createReactActivityDelegate() {
-    return new DefaultReactActivityDelegate(
-        this,
-        getMainComponentName(),
-        // If you opted-in for the New Architecture, we enable the Fabric Renderer.
-        DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
-        // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
-        DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
-        );
-  }
-}

+ 0 - 62
android/app/src/main/java/com/una/android/MainApplication.java

@@ -1,62 +0,0 @@
-package com.una.android;
-
-import android.app.Application;
-import com.facebook.react.PackageList;
-import com.facebook.react.ReactApplication;
-import com.facebook.react.ReactNativeHost;
-import com.facebook.react.ReactPackage;
-import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
-import com.facebook.react.defaults.DefaultReactNativeHost;
-import com.facebook.soloader.SoLoader;
-import java.util.List;
-
-public class MainApplication extends Application implements ReactApplication {
-
-  private final ReactNativeHost mReactNativeHost =
-      new DefaultReactNativeHost(this) {
-        @Override
-        public boolean getUseDeveloperSupport() {
-          return BuildConfig.DEBUG;
-        }
-
-        @Override
-        protected List<ReactPackage> getPackages() {
-          @SuppressWarnings("UnnecessaryLocalVariable")
-          List<ReactPackage> packages = new PackageList(this).getPackages();
-          // Packages that cannot be autolinked yet can be added manually here, for example:
-          // packages.add(new MyReactNativePackage());
-          return packages;
-        }
-
-        @Override
-        protected String getJSMainModuleName() {
-          return "index";
-        }
-
-        @Override
-        protected boolean isNewArchEnabled() {
-          return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
-        }
-
-        @Override
-        protected Boolean isHermesEnabled() {
-          return BuildConfig.IS_HERMES_ENABLED;
-        }
-      };
-
-  @Override
-  public ReactNativeHost getReactNativeHost() {
-    return mReactNativeHost;
-  }
-
-  @Override
-  public void onCreate() {
-    super.onCreate();
-    SoLoader.init(this, /* native exopackage */ false);
-    if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
-      // If you opted-in for the New Architecture, we load the native entry point for this app.
-      DefaultNewArchitectureEntryPoint.load();
-    }
-    ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
-  }
-}

二进制
android/app/src/main/res/drawable-hdpi/splash_image.png


二进制
android/app/src/main/res/drawable-mdpi/splash_image.png


二进制
android/app/src/main/res/drawable-xhdpi/splash_image.png


二进制
android/app/src/main/res/drawable-xxhdpi/splash_image.png


+ 3 - 2
android/app/src/main/res/drawable/rn_edit_text_material.xml

@@ -17,10 +17,11 @@
        android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
        android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
        android:insetTop="@dimen/abc_edit_text_inset_top_material"
-       android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
+       android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
+       >
 
     <selector>
-        <!-- 
+        <!--
           This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
           The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
           NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'

+ 0 - 11
android/app/src/main/res/drawable/splashscreen.xml

@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item android:drawable="@color/splashprimary" />
-    <item>
-        <bitmap
-                android:gravity="center"
-                android:src="@drawable/splash_image" />
-    </item>
-
-</layer-list>

+ 0 - 15
android/app/src/main/res/layout/launch_screen.xml

@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <ImageView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_margin="0dp"
-        android:contentDescription="Splashscreen"
-        android:scaleType="fitCenter"
-        android:background="@color/splashprimary"
-        android:src="@drawable/splashscreen"/>
-</RelativeLayout>

二进制
android/app/src/main/res/mipmap-hdpi-v26/ic_foreground.png


+ 0 - 4
android/app/src/main/res/mipmap-hdpi-v26/ic_launcher.xml

@@ -1,4 +0,0 @@
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/iconBackground"/>
-    <foreground android:drawable="@mipmap/ic_foreground"/>
-</adaptive-icon>

二进制
android/app/src/main/res/mipmap-hdpi/ic_launcher.png


二进制
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png


二进制
android/app/src/main/res/mipmap-ldpi-v26/ic_foreground.png


+ 0 - 4
android/app/src/main/res/mipmap-ldpi-v26/ic_launcher.xml

@@ -1,4 +0,0 @@
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/iconBackground"/>
-    <foreground android:drawable="@mipmap/ic_foreground"/>
-</adaptive-icon>

二进制
android/app/src/main/res/mipmap-ldpi/ic_launcher.png


二进制
android/app/src/main/res/mipmap-mdpi-v26/ic_foreground.png


+ 0 - 4
android/app/src/main/res/mipmap-mdpi-v26/ic_launcher.xml

@@ -1,4 +0,0 @@
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/iconBackground"/>
-    <foreground android:drawable="@mipmap/ic_foreground"/>
-</adaptive-icon>

二进制
android/app/src/main/res/mipmap-mdpi/ic_launcher.png


二进制
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png


二进制
android/app/src/main/res/mipmap-xhdpi-v26/ic_foreground.png


+ 0 - 4
android/app/src/main/res/mipmap-xhdpi-v26/ic_launcher.xml

@@ -1,4 +0,0 @@
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/iconBackground"/>
-    <foreground android:drawable="@mipmap/ic_foreground"/>
-</adaptive-icon>

二进制
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


二进制
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


二进制
android/app/src/main/res/mipmap-xxhdpi-v26/ic_foreground.png


+ 0 - 4
android/app/src/main/res/mipmap-xxhdpi-v26/ic_launcher.xml

@@ -1,4 +0,0 @@
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/iconBackground"/>
-    <foreground android:drawable="@mipmap/ic_foreground"/>
-</adaptive-icon>

二进制
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


二进制
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


二进制
android/app/src/main/res/mipmap-xxxhdpi-v26/ic_foreground.png


+ 0 - 4
android/app/src/main/res/mipmap-xxxhdpi-v26/ic_launcher.xml

@@ -1,4 +0,0 @@
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/iconBackground"/>
-    <foreground android:drawable="@mipmap/ic_foreground"/>
-</adaptive-icon>

二进制
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


二进制
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 0 - 3
android/app/src/main/res/values/colors-icon.xml

@@ -1,3 +0,0 @@
-<resources>
-<color name ='iconBackground'>#FFFFFF</color> 
-</resources>

+ 0 - 4
android/app/src/main/res/values/colors-splash.xml

@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-    <color name="splashprimary">#FFFFFF</color>
-</resources>

+ 5 - 2
android/app/src/main/res/values/strings.xml

@@ -1,3 +1,6 @@
 <resources>
-    <string name="app_name">UNA.IO</string>
-</resources>
+  <string name="app_name">una-mobile-app</string>
+  <string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
+  <string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
+  <string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
+</resources>

+ 14 - 13
android/app/src/main/res/values/styles.xml

@@ -1,13 +1,14 @@
-<resources>
-
-    <style name="SplashScreenTheme" parent="SplashScreen_SplashTheme">
-        <item name="colorPrimaryDark">@color/splashprimary</item>
-    </style>
-
-    <!-- Base application theme. -->
-    <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
-        <!-- Customize your theme here. -->
-        <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
-    </style>
-
-</resources>
+<resources xmlns:tools="http://schemas.android.com/tools">
+  <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
+    <item name="android:enforceNavigationBarContrast" tools:targetApi="29">true</item>
+    <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
+    <item name="colorPrimary">@color/colorPrimary</item>
+    <item name="android:statusBarColor">#ffffff</item>
+  </style>
+  <style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
+    <item name="windowSplashScreenBackground">@color/splashscreen_background</item>
+    <item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
+    <item name="postSplashScreenTheme">@style/AppTheme</item>
+    <item name="android:windowSplashScreenBehavior">icon_preferred</item>
+  </style>
+</resources>

+ 0 - 20
android/app/src/release/java/com/una/ReactNativeFlipper.java

@@ -1,20 +0,0 @@
-/**
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * <p>This source code is licensed under the MIT license found in the LICENSE file in the root
- * directory of this source tree.
- */
-package com.una.android;
-
-import android.content.Context;
-import com.facebook.react.ReactInstanceManager;
-
-/**
- * Class responsible of loading Flipper inside your React Native application. This is the release
- * flavor of it so it's empty as we don't want to load Flipper.
- */
-public class ReactNativeFlipper {
-  public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
-    // Do nothing as we don't want to initialize Flipper on Release.
-  }
-}

+ 19 - 16
android/build.gradle

@@ -1,21 +1,24 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 
 buildscript {
-    ext {
-        buildToolsVersion = "34.0.0"
-        minSdkVersion = 21
-        compileSdkVersion = 34
-        targetSdkVersion = 34
+  repositories {
+    google()
+    mavenCentral()
+  }
+  dependencies {
+    classpath('com.android.tools.build:gradle')
+    classpath('com.facebook.react:react-native-gradle-plugin')
+    classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
+  }
+}
 
-        // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
-        ndkVersion = "23.1.7779620"
-    }
-    repositories {
-        google()
-        mavenCentral()
-    }
-    dependencies {
-        classpath("com.android.tools.build:gradle:7.3.1")
-        classpath("com.facebook.react:react-native-gradle-plugin")
-    }
+allprojects {
+  repositories {
+    google()
+    mavenCentral()
+    maven { url 'https://www.jitpack.io' }
+  }
 }
+
+apply plugin: "expo-root-project"
+apply plugin: "com.facebook.react.rootproject"

+ 26 - 10
android/gradle.properties

@@ -15,17 +15,15 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
 # When configured, Gradle will run in incubating parallel mode.
 # This option should only be used with decoupled projects. More details, visit
 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
+org.gradle.parallel=true
 
 # AndroidX package structure to make it clearer which packages are bundled with the
 # Android operating system, and which are packaged with your app's APK
 # https://developer.android.com/topic/libraries/support-library/androidx-rn
 android.useAndroidX=true
-# Automatically convert third-party libraries to use AndroidX
-android.enableJetifier=true
 
-# Version of flipper SDK to use with React Native
-FLIPPER_VERSION=0.125.0
+# Enable AAPT2 PNG crunching
+android.enablePngCrunchInReleaseBuilds=true
 
 # Use this property to specify which architecture you want to build.
 # You can also override it from the CLI using
@@ -37,13 +35,31 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
 # your application. You should enable this flag either if you want
 # to write custom TurboModules/Fabric components OR use libraries that
 # are providing them.
-newArchEnabled=false
+newArchEnabled=true
 
 # Use this property to enable or disable the Hermes JS engine.
 # If set to false, you will be using JSC instead.
 hermesEnabled=true
 
-MYAPP_RELEASE_STORE_FILE=una-release-key.keystore
-MYAPP_RELEASE_KEY_ALIAS=una
-MYAPP_RELEASE_STORE_PASSWORD=
-MYAPP_RELEASE_KEY_PASSWORD=
+# Use this property to enable edge-to-edge display support.
+# This allows your app to draw behind system bars for an immersive UI.
+# Note: Only works with ReactActivity and should not be used with custom Activity.
+edgeToEdgeEnabled=true
+
+# Enable GIF support in React Native images (~200 B increase)
+expo.gif.enabled=true
+# Enable webp support in React Native images (~85 KB increase)
+expo.webp.enabled=true
+# Enable animated webp support (~3.4 MB increase)
+# Disabled by default because iOS doesn't support animated webp
+expo.webp.animated=false
+
+# Enable network inspector
+EX_DEV_CLIENT_NETWORK_INSPECTOR=true
+
+# Use legacy packaging to compress native libraries in the resulting APK.
+expo.useLegacyPackaging=false
+
+# Specifies whether the app is configured to use edge-to-edge via the app config or plugin
+# WARNING: This property has been deprecated and will be removed in Expo SDK 55. Use `edgeToEdgeEnabled` or `react.edgeToEdgeEnabled` to determine whether the project is using edge-to-edge.
+expo.edgeToEdgeEnabled=true

二进制
android/gradle/wrapper/gradle-wrapper.jar


+ 3 - 1
android/gradle/wrapper/gradle-wrapper.properties

@@ -1,5 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 32 - 15
android/gradlew

@@ -15,6 +15,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+# SPDX-License-Identifier: Apache-2.0
+#
 
 ##############################################################################
 #
@@ -55,7 +57,7 @@
 #       Darwin, MinGW, and NonStop.
 #
 #   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 #       within the Gradle project.
 #
 #       You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,11 @@ do
     esac
 done
 
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum
@@ -114,7 +114,7 @@ case "$( uname )" in                #(
   NONSTOP* )        nonstop=true ;;
 esac
 
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+CLASSPATH="\\\"\\\""
 
 
 # Determine the Java command to use to start the JVM.
@@ -133,22 +133,29 @@ location of your Java installation."
     fi
 else
     JAVACMD=java
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
+    fi
 fi
 
 # Increase the maximum file descriptors if we can.
 if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
     case $MAX_FD in #(
       max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         MAX_FD=$( ulimit -H -n ) ||
             warn "Could not query maximum file descriptor limit"
     esac
     case $MAX_FD in  #(
       '' | soft) :;; #(
       *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
         ulimit -n "$MAX_FD" ||
             warn "Could not set maximum file descriptor limit to $MAX_FD"
     esac
@@ -193,18 +200,28 @@ if "$cygwin" || "$msys" ; then
     done
 fi
 
-# Collect all arguments for the java command;
-#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-#     shell script including quotes and variable substitutions, so put them in
-#     double quotes to make sure that they get re-expanded; and
-#   * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
 
 set -- \
         "-Dorg.gradle.appname=$APP_BASE_NAME" \
         -classpath "$CLASSPATH" \
-        org.gradle.wrapper.GradleWrapperMain \
+        -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
         "$@"
 
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
 # Use "xargs" to parse quoted args.
 #
 # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

+ 94 - 89
android/gradlew.bat

@@ -1,89 +1,94 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem      https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem  Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 38 - 3
android/settings.gradle

@@ -1,4 +1,39 @@
-rootProject.name = 'una'
-apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
+pluginManagement {
+  def reactNativeGradlePlugin = new File(
+    providers.exec {
+      workingDir(rootDir)
+      commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
+    }.standardOutput.asText.get().trim()
+  ).getParentFile().absolutePath
+  includeBuild(reactNativeGradlePlugin)
+  
+  def expoPluginsPath = new File(
+    providers.exec {
+      workingDir(rootDir)
+      commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
+    }.standardOutput.asText.get().trim(),
+    "../android/expo-gradle-plugin"
+  ).absolutePath
+  includeBuild(expoPluginsPath)
+}
+
+plugins {
+  id("com.facebook.react.settings")
+  id("expo-autolinking-settings")
+}
+
+extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
+  if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
+    ex.autolinkLibrariesFromCommand()
+  } else {
+    ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
+  }
+}
+expoAutolinking.useExpoModules()
+
+rootProject.name = 'una-mobile-app'
+
+expoAutolinking.useExpoVersionCatalog()
+
 include ':app'
-includeBuild('../node_modules/react-native-gradle-plugin')
+includeBuild(expoAutolinking.reactNativeGradlePlugin)

+ 65 - 3
app.json

@@ -1,4 +1,66 @@
 {
-  "name": "una",
-  "displayName": "una"
-}
+  "expo": {
+    "name": "una-mobile-app",
+    "slug": "una-mobile-app",
+    "version": "3.0.0",
+    "orientation": "portrait",
+    "icon": "./assets/images/icon.png",
+    "scheme": "unacms",
+    "userInterfaceStyle": "automatic",
+    "newArchEnabled": true,
+    "ios": {
+      "bundleIdentifier": "com.unacms.app",
+      "infoPlist": {
+          "UIBackgroundModes": ["remote-notification"],
+          "NSAppTransportSecurity": {"NSAllowsArbitraryLoads": true}
+      },
+      "entitlements": {
+          "aps-environment": "development",
+          "com.apple.security.application-groups": [
+              "group.${ios.bundleIdentifier}.onesignal"
+          ]
+      },
+      "supportsTablet": true
+    },
+    "android": {
+      "package": "com.unacms.app",
+      "adaptiveIcon": {
+        "backgroundColor": "#E6F4FE",
+        "foregroundImage": "./assets/images/android-icon-foreground.png",
+        "backgroundImage": "./assets/images/android-icon-background.png",
+        "monochromeImage": "./assets/images/android-icon-monochrome.png"
+      },
+      "edgeToEdgeEnabled": true,
+      "predictiveBackGestureEnabled": false
+    },
+    "web": {
+      "output": "static",
+      "favicon": "./assets/images/favicon.png"
+    },
+    "plugins": [
+      [
+          "onesignal-expo-plugin",
+          {
+            "mode": "development"
+          }
+      ],
+      "expo-router",
+      [
+        "expo-splash-screen",
+        {
+          "image": "./assets/images/splash-icon.png",
+          "imageWidth": 200,
+          "resizeMode": "contain",
+          "backgroundColor": "#ffffff",
+          "dark": {
+            "backgroundColor": "#000000"
+          }
+        }
+      ]
+    ],
+    "experiments": {
+      "typedRoutes": true,
+      "reactCompiler": true
+    }
+  }
+}

+ 24 - 0
app/1111_layout.tsx

@@ -0,0 +1,24 @@
+import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
+import { Stack } from 'expo-router';
+import { StatusBar } from 'expo-status-bar';
+import 'react-native-reanimated';
+
+import { useColorScheme } from '@/hooks/use-color-scheme';
+
+export const unstable_settings = {
+  anchor: '(tabs)',
+};
+
+export default function RootLayout() {
+  const colorScheme = useColorScheme();
+
+  return (
+    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
+      <Stack>
+        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
+        <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
+      </Stack>
+      <StatusBar style="auto" />
+    </ThemeProvider>
+  );
+}

+ 225 - 0
app/index.tsx

@@ -0,0 +1,225 @@
+import { useEffect, useRef, useState } from "react";
+import { BackHandler, Platform, StyleSheet, View } from "react-native";
+import { LogLevel, OneSignal } from 'react-native-onesignal';
+import { WebView, WebViewNavigation } from "react-native-webview";
+import BottomTabs from "../components/bottom-tabs";
+import Header from "../components/header";
+import WebViewContainer from "../components/web-view";
+import { ONESIGNAL_APP_ID } from "../constants/settings";
+import { WebViewHandlers } from "../types/webview-handlers-type";
+import { requestAndroidPermissions } from "../utils/android-permissins-request";
+
+export default function App() {
+    const webViewRef = useRef<WebView>(null);
+
+    const [loggedin, setLoggedIn] = useState(false);
+    const [title, setTitle] = useState("");
+    const [loading, setLoadingState] = useState(false);
+    const [canGoBack, setCanGoBack] = useState(false);
+    const [data, setData] = useState<any>(null);
+    const [backButtonEnabled, setBackButtonEnabled] = useState(true);
+
+    // android permissions request
+    useEffect(() => {
+        requestAndroidPermissions();
+    }, []);
+
+    // OneSignal initialization
+    useEffect(() => {        
+        OneSignal.Debug.setLogLevel(LogLevel.Verbose);
+        OneSignal.setConsentRequired(true);
+        OneSignal.initialize(ONESIGNAL_APP_ID);
+
+        OneSignal.Notifications.addEventListener('click', (event:any) => {
+            console.log('OneSignal: notification clicked:', event);
+            const url = event.notification?.additionalData?.url;
+            if (url && webViewRef.current) {
+                webViewRef.current.injectJavaScript(`window.location = '${url}';`);
+            }            
+        });
+    }, []);
+
+    // Android hardware back button
+    useEffect(() => {        
+        let backHandler: any;
+        if (Platform.OS === "android") {
+            backHandler = BackHandler.addEventListener("hardwareBackPress", () => {
+                if (backButtonEnabled && webViewRef.current) {
+                    webViewRef.current.goBack();
+                    return true;
+                }
+                return false;
+            });
+        }
+
+        // cleanup
+        return () => {
+            if (Platform.OS === "android" && backHandler) {
+                backHandler.remove();
+            }
+        };
+    }, [backButtonEnabled]);
+
+    // Handle navigation changes 
+    const handleNavChange = (nav: WebViewNavigation) => {
+        setCanGoBack(nav.canGoBack);
+        setTitle(nav.title || ""); // replaced with 'handleTitleChange'
+    };
+
+    // All WebViewHandlers
+    const webViewHandlers: WebViewHandlers = {      
+        setLoading: (b:boolean) => {
+            setLoadingState(b);
+        },
+        setData,
+        setLoggedIn,
+        onVideoCallStart: (uri, audio) => {
+            console.log("Video call start:", uri, audio);
+            // Add your actual video call logic here
+        },
+        endVideoCall: () => {
+            console.log("Video call stop");
+        },
+        onRequestPurchase: (payload) => {
+            console.log("Purchase request:", payload);
+        },
+        onRequestSubscription: (payload) => {
+            console.log("Subscription request:", payload);
+        },
+        onHomeMenu: () => {
+            console.log("Goto home");
+            webViewRef.current?.reload(); // Example: reload home page
+        },
+        reload: () => {
+            webViewRef.current?.reload();
+        },
+        onStateChange: handleNavChange,
+        onLoadProgress: (progress) => setLoadingState(progress < 1),
+        //onTitleChange: (newTitle) => setTitle(newTitle),
+    };
+
+    return (
+      <View style={styles.container}>
+        <Header
+          title={title}
+          loading={loading}
+          canGoBack={canGoBack}
+          onBack={() => webViewRef.current?.goBack()}
+        />
+
+        <WebViewContainer
+          ref={webViewRef}
+          {...webViewHandlers}
+        />
+
+        {loggedin === true && (
+          <BottomTabs webViewRef={webViewRef} data={data} />
+        )}
+      </View>
+    );
+}
+
+const styles = StyleSheet.create({
+  container: { flex: 1 },
+});
+
+/*
+import { Image } from 'expo-image';
+import { Platform, StyleSheet } from 'react-native';
+
+import { HelloWave } from '@/components/hello-wave';
+import ParallaxScrollView from '@/components/parallax-scroll-view';
+import { ThemedText } from '@/components/themed-text';
+import { ThemedView } from '@/components/themed-view';
+import { Link } from 'expo-router';
+
+export default function HomeScreen() {
+  return (
+    <ParallaxScrollView
+      headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
+      headerImage={
+        <Image
+          source={require('@/assets/images/partial-react-logo.png')}
+          style={styles.reactLogo}
+        />
+      }>
+      <ThemedView style={styles.titleContainer}>
+        <ThemedText type="title">Welcome!</ThemedText>
+        <HelloWave />
+      </ThemedView>
+      <ThemedView style={styles.stepContainer}>
+        <ThemedText type="subtitle">Step 1: Try it</ThemedText>
+        <ThemedText>
+          Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
+          Press{' '}
+          <ThemedText type="defaultSemiBold">
+            {Platform.select({
+              ios: 'cmd + d',
+              android: 'cmd + m',
+              web: 'F12',
+            })}
+          </ThemedText>{' '}
+          to open developer tools.
+        </ThemedText>
+      </ThemedView>
+      <ThemedView style={styles.stepContainer}>
+        <Link href="/modal">
+          <Link.Trigger>
+            <ThemedText type="subtitle">Step 2: Explore</ThemedText>
+          </Link.Trigger>
+          <Link.Preview />
+          <Link.Menu>
+            <Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} />
+            <Link.MenuAction
+              title="Share"
+              icon="square.and.arrow.up"
+              onPress={() => alert('Share pressed')}
+            />
+            <Link.Menu title="More" icon="ellipsis">
+              <Link.MenuAction
+                title="Delete"
+                icon="trash"
+                destructive
+                onPress={() => alert('Delete pressed')}
+              />
+            </Link.Menu>
+          </Link.Menu>
+        </Link>
+
+        <ThemedText>
+          {`Tap the Explore tab to learn more about what's included in this starter app.`}
+        </ThemedText>
+      </ThemedView>
+      <ThemedView style={styles.stepContainer}>
+        <ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
+        <ThemedText>
+          {`When you're ready, run `}
+          <ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
+          <ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
+          <ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
+          <ThemedText type="defaultSemiBold">app-example</ThemedText>.
+        </ThemedText>
+      </ThemedView>
+    </ParallaxScrollView>
+  );
+}
+
+const styles = StyleSheet.create({
+  titleContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 8,
+  },
+  stepContainer: {
+    gap: 8,
+    marginBottom: 8,
+  },
+  reactLogo: {
+    height: 178,
+    width: 290,
+    bottom: 0,
+    left: 0,
+    position: 'absolute',
+  },
+});
+*/

+ 29 - 0
app/modal.tsx

@@ -0,0 +1,29 @@
+import { Link } from 'expo-router';
+import { StyleSheet } from 'react-native';
+
+import { ThemedText } from '@/components/themed-text';
+import { ThemedView } from '@/components/themed-view';
+
+export default function ModalScreen() {
+  return (
+    <ThemedView style={styles.container}>
+      <ThemedText type="title">This is a modal</ThemedText>
+      <Link href="/" dismissTo style={styles.link}>
+        <ThemedText type="link">Go to home screen</ThemedText>
+      </Link>
+    </ThemedView>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+    padding: 20,
+  },
+  link: {
+    marginTop: 15,
+    paddingVertical: 15,
+  },
+});

二进制
assets/images/android-icon-background.png


二进制
assets/images/android-icon-foreground.png


二进制
assets/images/android-icon-monochrome.png


二进制
assets/images/favicon.png


二进制
assets/images/icon.png


二进制
assets/images/partial-react-logo.png


二进制
assets/images/react-logo.png


二进制
assets/images/react-logo@2x.png


二进制
assets/images/react-logo@3x.png


二进制
assets/images/splash-icon.png


+ 0 - 3
babel.config.js

@@ -1,3 +0,0 @@
-module.exports = {
-  presets: ['module:metro-react-native-babel-preset'],
-};

+ 99 - 0
components/bottom-tabs.tsx

@@ -0,0 +1,99 @@
+import { RefObject } from "react";
+import { Pressable, StyleSheet, Text, View } from "react-native";
+import { WebView } from "react-native-webview";
+import { UNA_URL } from "../constants/settings";
+import { Bars, Bell, Chat, Plus, User } from "./icons";
+
+type Props = {
+  webViewRef: RefObject<WebView>;
+  data: any;
+};
+
+export default function BottomTabs({ webViewRef, data }: Props) {
+  const runFuncJS = (js: string) => {
+    webViewRef.current?.injectJavaScript(`
+      if (window.${js}) {
+        ${js}();
+      }
+      true;
+    `);
+  };
+
+  const runJS = (js: string) => {
+    webViewRef.current?.injectJavaScript(`      
+        ${js};
+        true;
+    `);
+  };
+
+  const renderBadge = (number: number) => {
+    if (number <= 0) return null;
+    return (
+      <View style={styles.badge}>
+        <Text style={styles.badgeText}>{number}</Text>
+      </View>
+    );
+  };
+
+  return (
+    <View style={styles.container}>
+      <Pressable onPress={() => runFuncJS("bx_mobile_apps_show_main_menu")}>
+        <Bars size={24} color="#111" />
+      </Pressable>
+
+      <Pressable onPress={() => runJS(`document.location = "${UNA_URL}/page.php?i=notifications-view"`)}>
+        <Bell size={24} color="#111" />
+        {renderBadge(data.bubbles?.['notifications-preview'] ?? 0)}
+      </Pressable>
+
+      <Pressable onPress={() => runFuncJS("bx_mobile_apps_show_add_menu")}>
+        <Plus size={24} color="#111" />
+      </Pressable>
+
+      <Pressable onPress={() => runFuncJS("bx_mobile_apps_show_messenger_menu")}>
+        <Chat size={24} color="#111" />
+        {renderBadge(data.bubbles?.['notifications-messenger'] ?? 0)}
+      </Pressable>
+
+      <Pressable onPress={() => runFuncJS("bx_mobile_apps_show_profile_menu")}>
+        <User size={24} color="#111" />
+        {renderBadge(data.bubbles?.account ?? 0)}
+      </Pressable>
+
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    height: 56,
+    flexDirection: "row",
+    justifyContent: "space-around",
+    alignItems: "center",
+    borderTopWidth: 1,
+    borderColor: "#ddd",
+    backgroundColor: "#fff",
+  },
+  tab: {
+    justifyContent: "center",
+    alignItems: "center",
+    position: "relative",
+  },
+  badge: {
+    position: "absolute",
+    top: -6,
+    right: -10,
+    backgroundColor: "red",
+    borderRadius: 8,
+    minWidth: 16,
+    height: 16,
+    justifyContent: "center",
+    alignItems: "center",
+    paddingHorizontal: 3,
+  },
+  badgeText: {
+    color: "#fff",
+    fontSize: 10,
+    fontWeight: "bold",
+  },
+});

+ 25 - 0
components/external-link.tsx

@@ -0,0 +1,25 @@
+import { Href, Link } from 'expo-router';
+import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser';
+import { type ComponentProps } from 'react';
+
+type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: Href & string };
+
+export function ExternalLink({ href, ...rest }: Props) {
+  return (
+    <Link
+      target="_blank"
+      {...rest}
+      href={href}
+      onPress={async (event) => {
+        if (process.env.EXPO_OS !== 'web') {
+          // Prevent the default behavior of linking to the default browser on native.
+          event.preventDefault();
+          // Open the link in an in-app browser.
+          await openBrowserAsync(href, {
+            presentationStyle: WebBrowserPresentationStyle.AUTOMATIC,
+          });
+        }
+      }}
+    />
+  );
+}

+ 18 - 0
components/haptic-tab.tsx

@@ -0,0 +1,18 @@
+import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
+import { PlatformPressable } from '@react-navigation/elements';
+import * as Haptics from 'expo-haptics';
+
+export function HapticTab(props: BottomTabBarButtonProps) {
+  return (
+    <PlatformPressable
+      {...props}
+      onPressIn={(ev) => {
+        if (process.env.EXPO_OS === 'ios') {
+          // Add a soft haptic feedback when pressing down on the tabs.
+          Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+        }
+        props.onPressIn?.(ev);
+      }}
+    />
+  );
+}

+ 60 - 0
components/header.tsx

@@ -0,0 +1,60 @@
+import { ActivityIndicator, Pressable, StyleSheet, Text, View } from "react-native";
+
+type Props = {
+  title: string;
+  loading: boolean;
+  canGoBack: boolean;
+  onBack: () => void;
+};
+
+export default function Header({ title, loading, canGoBack, onBack }: Props) {
+  return (
+    <View style={styles.container}>
+      <Pressable
+        onPress={onBack}
+        disabled={!canGoBack}
+        style={styles.back}
+      >
+        <Text style={[styles.backText, !canGoBack && styles.disabled]}>
+          ←
+        </Text>
+      </Pressable>
+
+      <Text numberOfLines={1} style={styles.title}>
+        {title || " "}
+      </Text>
+
+      <View style={styles.right}>
+        {loading && <ActivityIndicator size="small" />}
+      </View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    height: 56,
+    flexDirection: "row",
+    alignItems: "center",
+    paddingHorizontal: 12,
+    borderBottomWidth: 1,
+    borderColor: "#ddd",
+    backgroundColor: "#fff",
+  },
+  back: {
+    width: 40,
+    justifyContent: "center",
+    alignItems: "center",
+  },
+  backText: { fontSize: 20 },
+  disabled: { opacity: 0.3 },
+  title: {
+    flex: 1,
+    fontSize: 16,
+    fontWeight: "600",
+  },
+  right: {
+    width: 40,
+    alignItems: "center",
+  },
+});

+ 19 - 0
components/hello-wave.tsx

@@ -0,0 +1,19 @@
+import Animated from 'react-native-reanimated';
+
+export function HelloWave() {
+  return (
+    <Animated.Text
+      style={{
+        fontSize: 28,
+        lineHeight: 32,
+        marginTop: -6,
+        animationName: {
+          '50%': { transform: [{ rotate: '25deg' }] },
+        },
+        animationIterationCount: 4,
+        animationDuration: '300ms',
+      }}>
+      👋
+    </Animated.Text>
+  );
+}

+ 181 - 0
components/icons.tsx

@@ -0,0 +1,181 @@
+import React from "react";
+import Svg, {
+  Defs,
+  G,
+  LinearGradient,
+  Path,
+  RadialGradient,
+  Stop,
+} from "react-native-svg";
+
+export type IconProps = {
+  size?: number;
+  color?: string;
+  style?: any;
+};
+
+/* ===================== LOGO ===================== */
+
+export function Logo({ size = 64, style }: IconProps) {
+  return (
+    <Svg
+      width={size}
+      height={(size * 1024) / 3264}
+      viewBox="0 0 3264 1024"
+      style={style}
+    >
+      <Path d="M840.961 925.025..." fill="url(#a)" />
+      <Path d="M181.2 100.447..." fill="url(#b)" />
+      <Path d="M512 992..." fill="url(#c)" />
+      <Path d="M709.38 302.273..." fill="url(#d)" />
+      <Path d="M1706.33 771.031..." fill="url(#e)" />
+      <Path d="M2123.11 242.93..." fill="url(#f)" />
+      <Path d="M3263.25 501.331..." fill="url(#g)" />
+
+      <Defs>
+        <RadialGradient id="a" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 260 -165.059 0 992 732)">
+          <Stop stopColor="#FB923C" />
+          <Stop offset={1} stopColor="#EF4444" />
+        </RadialGradient>
+
+        <RadialGradient id="b" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 -260 171.858 0 32 291.999)">
+          <Stop stopColor="#0EA5E9" />
+          <Stop offset={1} stopColor="#38BDF8" />
+        </RadialGradient>
+
+        <RadialGradient id="c" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 864 -719.971 0 632 128)">
+          <Stop stopColor="#38BDF8" />
+          <Stop offset={1} stopColor="#0284C7" />
+        </RadialGradient>
+
+        <RadialGradient id="d" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 864 -719.971 0 525.5 32)">
+          <Stop stopColor="#34D399" />
+          <Stop offset={1} stopColor="#059669" />
+        </RadialGradient>
+
+        <LinearGradient id="e" x1="1566" y1="203" x2="1566" y2="805">
+          <Stop stopColor="#F59E0B" />
+          <Stop offset={1} stopColor="#F97316" />
+        </LinearGradient>
+
+        <LinearGradient id="f" x1="2263" y1="208" x2="2263" y2="810">
+          <Stop stopColor="#F59E0B" />
+          <Stop offset={1} stopColor="#F97316" />
+        </LinearGradient>
+
+        <LinearGradient id="g" x1="2962" y1="208" x2="2962" y2="812">
+          <Stop stopColor="#F59E0B" />
+          <Stop offset={1} stopColor="#F97316" />
+        </LinearGradient>
+      </Defs>
+    </Svg>
+  );
+}
+
+/* ===================== ICON TEMPLATE ===================== */
+
+function StrokeIcon({
+  size = 24,
+  color = "#000",
+  style,
+  children,
+}: IconProps & { children: React.ReactNode }) {
+  return (
+    <Svg width={size} height={size} viewBox="0 0 24 24" style={style}>
+      <G fill="none" stroke={color} strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
+        {children}
+      </G>
+    </Svg>
+  );
+}
+
+/* ===================== ICONS ===================== */
+
+export const Home = (p: IconProps) => (
+  <StrokeIcon {...p}>
+    <Path d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12" />
+    <Path d="M4.5 9.75V19.875A1.125 1.125 0 005.625 21H9.75v-4.875A1.125 1.125 0 0110.875 15h2.25A1.125 1.125 0 0114.25 16.125V21h4.125A1.125 1.125 0 0019.5 19.875V9.75" />
+  </StrokeIcon>
+);
+
+export const Apps = (p: IconProps) => (
+  <StrokeIcon {...p}>
+    <Path d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25A2.25 2.25 0 018.25 10.5H6A2.25 2.25 0 013.75 8.25V6z" />
+    <Path d="M3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25z" />
+    <Path d="M13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6z" />
+    <Path d="M13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
+  </StrokeIcon>
+);
+
+export const Bell = (p: IconProps) => (
+  <StrokeIcon {...p}>
+      <G fill="none" stroke-width="1.5" stroke={p.color} class="w-6 h-6">
+          <Path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
+      </G>
+  </StrokeIcon>
+);
+
+export const Plus = (p: IconProps) => (
+  <StrokeIcon {...p}>
+    <Path d="M12 9v6M15 12H9" />
+    <Path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
+  </StrokeIcon>
+);
+
+export const Chat = (p: IconProps) => (
+  <StrokeIcon {...p}>
+      <G fill="none" stroke-width="1.5" stroke={p.color} class="w-6 h-6">
+          <Path stroke-linecap="round" stroke-linejoin="round" d="M8.625 9.75a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375m-13.5 3.01c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 01.778-.332 48.294 48.294 0 005.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
+      </G>
+  </StrokeIcon>
+);
+
+export const User = (p: IconProps) => (
+  <StrokeIcon {...p}>
+    <Path d="M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
+    <Path d="M6.018 18.725A7.488 7.488 0 0112 15.75a7.488 7.488 0 015.982 2.975" />
+  </StrokeIcon>
+);
+
+export const Bars = (p: IconProps) => (
+  <StrokeIcon {...p}>
+    <Path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
+  </StrokeIcon>
+);
+
+export const Search = (p: IconProps) => (
+  <StrokeIcon {...p}>
+    <Path d="M21 21l-5.197-5.197" />
+    <Path d="M15.803 15.803A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
+  </StrokeIcon>
+);
+
+export const Back = (p: IconProps) => (
+  <StrokeIcon {...p}>
+    <Path d="M10.5 19.5L3 12l7.5-7.5M3 12h18" />
+  </StrokeIcon>
+);
+
+export const Cross = (p: IconProps) => (
+  <StrokeIcon {...p}>
+    <Path d="M6 18L18 6M6 6l12 12" />
+  </StrokeIcon>
+);
+
+export const In = (p: IconProps) => (
+  <StrokeIcon {...p}>
+      <G fill="none" strokeWidth={1.5} stroke={p.color} className="w-6 h-6">
+          <Path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
+      </G>
+  </StrokeIcon>
+);
+
+export const PlusCircle = Plus;
+
+export const Question = (p: IconProps) => (
+  <StrokeIcon {...p}>
+    <Path d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75" />
+    <Path d="M12 17.25h.008" />
+    <Path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
+  </StrokeIcon>
+);

+ 79 - 0
components/parallax-scroll-view.tsx

@@ -0,0 +1,79 @@
+import type { PropsWithChildren, ReactElement } from 'react';
+import { StyleSheet } from 'react-native';
+import Animated, {
+  interpolate,
+  useAnimatedRef,
+  useAnimatedStyle,
+  useScrollOffset,
+} from 'react-native-reanimated';
+
+import { ThemedView } from '@/components/themed-view';
+import { useColorScheme } from '@/hooks/use-color-scheme';
+import { useThemeColor } from '@/hooks/use-theme-color';
+
+const HEADER_HEIGHT = 250;
+
+type Props = PropsWithChildren<{
+  headerImage: ReactElement;
+  headerBackgroundColor: { dark: string; light: string };
+}>;
+
+export default function ParallaxScrollView({
+  children,
+  headerImage,
+  headerBackgroundColor,
+}: Props) {
+  const backgroundColor = useThemeColor({}, 'background');
+  const colorScheme = useColorScheme() ?? 'light';
+  const scrollRef = useAnimatedRef<Animated.ScrollView>();
+  const scrollOffset = useScrollOffset(scrollRef);
+  const headerAnimatedStyle = useAnimatedStyle(() => {
+    return {
+      transform: [
+        {
+          translateY: interpolate(
+            scrollOffset.value,
+            [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
+            [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
+          ),
+        },
+        {
+          scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
+        },
+      ],
+    };
+  });
+
+  return (
+    <Animated.ScrollView
+      ref={scrollRef}
+      style={{ backgroundColor, flex: 1 }}
+      scrollEventThrottle={16}>
+      <Animated.View
+        style={[
+          styles.header,
+          { backgroundColor: headerBackgroundColor[colorScheme] },
+          headerAnimatedStyle,
+        ]}>
+        {headerImage}
+      </Animated.View>
+      <ThemedView style={styles.content}>{children}</ThemedView>
+    </Animated.ScrollView>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+  header: {
+    height: HEADER_HEIGHT,
+    overflow: 'hidden',
+  },
+  content: {
+    flex: 1,
+    padding: 32,
+    gap: 16,
+    overflow: 'hidden',
+  },
+});

+ 60 - 0
components/themed-text.tsx

@@ -0,0 +1,60 @@
+import { StyleSheet, Text, type TextProps } from 'react-native';
+
+import { useThemeColor } from '@/hooks/use-theme-color';
+
+export type ThemedTextProps = TextProps & {
+  lightColor?: string;
+  darkColor?: string;
+  type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
+};
+
+export function ThemedText({
+  style,
+  lightColor,
+  darkColor,
+  type = 'default',
+  ...rest
+}: ThemedTextProps) {
+  const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
+
+  return (
+    <Text
+      style={[
+        { color },
+        type === 'default' ? styles.default : undefined,
+        type === 'title' ? styles.title : undefined,
+        type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
+        type === 'subtitle' ? styles.subtitle : undefined,
+        type === 'link' ? styles.link : undefined,
+        style,
+      ]}
+      {...rest}
+    />
+  );
+}
+
+const styles = StyleSheet.create({
+  default: {
+    fontSize: 16,
+    lineHeight: 24,
+  },
+  defaultSemiBold: {
+    fontSize: 16,
+    lineHeight: 24,
+    fontWeight: '600',
+  },
+  title: {
+    fontSize: 32,
+    fontWeight: 'bold',
+    lineHeight: 32,
+  },
+  subtitle: {
+    fontSize: 20,
+    fontWeight: 'bold',
+  },
+  link: {
+    lineHeight: 30,
+    fontSize: 16,
+    color: '#0a7ea4',
+  },
+});

+ 14 - 0
components/themed-view.tsx

@@ -0,0 +1,14 @@
+import { View, type ViewProps } from 'react-native';
+
+import { useThemeColor } from '@/hooks/use-theme-color';
+
+export type ThemedViewProps = ViewProps & {
+  lightColor?: string;
+  darkColor?: string;
+};
+
+export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
+  const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
+
+  return <View style={[{ backgroundColor }, style]} {...otherProps} />;
+}

+ 45 - 0
components/ui/collapsible.tsx

@@ -0,0 +1,45 @@
+import { PropsWithChildren, useState } from 'react';
+import { StyleSheet, TouchableOpacity } from 'react-native';
+
+import { ThemedText } from '@/components/themed-text';
+import { ThemedView } from '@/components/themed-view';
+import { IconSymbol } from '@/components/ui/icon-symbol';
+import { Colors } from '@/constants/theme';
+import { useColorScheme } from '@/hooks/use-color-scheme';
+
+export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
+  const [isOpen, setIsOpen] = useState(false);
+  const theme = useColorScheme() ?? 'light';
+
+  return (
+    <ThemedView>
+      <TouchableOpacity
+        style={styles.heading}
+        onPress={() => setIsOpen((value) => !value)}
+        activeOpacity={0.8}>
+        <IconSymbol
+          name="chevron.right"
+          size={18}
+          weight="medium"
+          color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
+          style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }}
+        />
+
+        <ThemedText type="defaultSemiBold">{title}</ThemedText>
+      </TouchableOpacity>
+      {isOpen && <ThemedView style={styles.content}>{children}</ThemedView>}
+    </ThemedView>
+  );
+}
+
+const styles = StyleSheet.create({
+  heading: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 6,
+  },
+  content: {
+    marginTop: 6,
+    marginLeft: 24,
+  },
+});

+ 32 - 0
components/ui/icon-symbol.ios.tsx

@@ -0,0 +1,32 @@
+import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
+import { StyleProp, ViewStyle } from 'react-native';
+
+export function IconSymbol({
+  name,
+  size = 24,
+  color,
+  style,
+  weight = 'regular',
+}: {
+  name: SymbolViewProps['name'];
+  size?: number;
+  color: string;
+  style?: StyleProp<ViewStyle>;
+  weight?: SymbolWeight;
+}) {
+  return (
+    <SymbolView
+      weight={weight}
+      tintColor={color}
+      resizeMode="scaleAspectFit"
+      name={name}
+      style={[
+        {
+          width: size,
+          height: size,
+        },
+        style,
+      ]}
+    />
+  );
+}

+ 41 - 0
components/ui/icon-symbol.tsx

@@ -0,0 +1,41 @@
+// Fallback for using MaterialIcons on Android and web.
+
+import MaterialIcons from '@expo/vector-icons/MaterialIcons';
+import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
+import { ComponentProps } from 'react';
+import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
+
+type IconMapping = Record<SymbolViewProps['name'], ComponentProps<typeof MaterialIcons>['name']>;
+type IconSymbolName = keyof typeof MAPPING;
+
+/**
+ * Add your SF Symbols to Material Icons mappings here.
+ * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
+ * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
+ */
+const MAPPING = {
+  'house.fill': 'home',
+  'paperplane.fill': 'send',
+  'chevron.left.forwardslash.chevron.right': 'code',
+  'chevron.right': 'chevron-right',
+} as IconMapping;
+
+/**
+ * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
+ * This ensures a consistent look across platforms, and optimal resource usage.
+ * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
+ */
+export function IconSymbol({
+  name,
+  size = 24,
+  color,
+  style,
+}: {
+  name: IconSymbolName;
+  size?: number;
+  color: string | OpaqueColorValue;
+  style?: StyleProp<TextStyle>;
+  weight?: SymbolWeight;
+}) {
+  return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;
+}

+ 44 - 0
components/web-view-error-screen.tsx

@@ -0,0 +1,44 @@
+import React, { ReactNode } from "react";
+import { Button, StyleSheet, Text, View } from "react-native";
+import { WebViewHandlers } from "../types/webview-handlers-type";
+
+/**
+ * Render error screen for WebView
+ * @param handlers WebViewHandlers to access reload and other functions
+ * @param errorDomain error domain provided by WebView
+ * @param errorCode error code provided by WebView
+ * @param errorDesc error description
+ */
+export function renderWebViewError(
+    handlers: WebViewHandlers,
+    errorDomain: any,
+    errorCode: any,
+    errorDesc: string
+): ReactNode {
+    return (
+        <View style={styles.errorContainer}>
+            <Text style={styles.errorText}>Oops! Something went wrong.</Text>
+            <Text style={styles.errorDesc}>{errorDesc}</Text>
+            <Button title="Reload" onPress={handlers.reload} />
+        </View>
+    );
+}
+
+const styles = StyleSheet.create({
+    errorContainer: {
+        flex: 9999999,
+        justifyContent: "center",
+        alignItems: "center",
+        padding: 16,
+    },
+    errorText: {
+        fontSize: 18,
+        fontWeight: "bold",
+        marginBottom: 8,
+    },
+    errorDesc: {
+        fontSize: 14,
+        marginBottom: 16,
+        textAlign: "center",
+    },
+});

+ 44 - 0
components/web-view-load-with-request.tsx

@@ -0,0 +1,44 @@
+import { Linking, Platform } from "react-native";
+import { EXCEPTION_URLS, UNA_URL } from "../constants/settings";
+
+export function shouldStartLoadWithRequest(event: {
+    url: string;
+    navigationType?: string;
+}): boolean {
+
+    const url = event.url;
+
+    // block blank pages
+    if (url === "about:blank") return false;
+
+    // check for exception URLs
+    const isException = EXCEPTION_URLS.some(e => {
+        url.indexOf(e) !== -1
+    });
+
+    // allow internal URLs
+    const isInternal = url.indexOf(UNA_URL) !== -1;
+
+    // special Android/iOS conditions
+    const isAndroid = Platform.OS === "android";
+    const isIosClick = Platform.OS === "ios" && event.navigationType === "click";
+
+    // block & open external URLs
+    if ((url.indexOf("http") !== 0 || (!isException && !isInternal && (isAndroid || isIosClick))) 
+        || url.indexOf("calendar_sync") !== -1) {
+
+        Linking.canOpenURL(url)
+            .then(supported => {
+                if (supported) {
+                    Linking.openURL(url);
+                } else {
+                    console.warn("Don't know how to open URI:", url);
+                }
+            })
+            .catch(err => console.warn("Error opening URL:", url, err));
+
+        return false; // prevent WebView from loading
+    }
+
+    return true; // allow WebView to load
+}

+ 50 - 0
components/web-view.tsx

@@ -0,0 +1,50 @@
+import { forwardRef } from "react";
+import { Platform, StyleSheet, View } from "react-native";
+import { WebView } from "react-native-webview";
+import { UNA_URL } from "../constants/settings";
+import injectedJS from "../injected/webview-injected"; // string, bundled at compile time
+import { WebViewHandlers } from "../types/webview-handlers-type";
+import { handleWebViewMessage } from "../utils/webview-message-handler";
+import { renderWebViewError } from "./web-view-error-screen";
+import { shouldStartLoadWithRequest } from "./web-view-load-with-request";
+
+const WebViewContainer = forwardRef<WebView, WebViewHandlers>((handlers, ref) => {
+    return (
+      <View style={styles.container}>
+        <WebView
+            ref={ref}
+            source={{ uri: UNA_URL }}
+            userAgent={"UNAMobileApp/Mobile (" + Platform.OS + ")"}
+            injectedJavaScript={injectedJS}
+
+            useWebKit={true}
+            allowFileAccess={true}
+            startInLoadingState={false}
+            setSupportMultipleWindows={false}
+            hideKeyboardAccessoryView={true}
+            allowsLinkPreview={true}
+
+            javaScriptEnabled
+            geolocationEnabled
+            domStorageEnabled
+            pullToRefreshEnabled // iOS only
+            webviewDebuggingEnabled
+
+            onNavigationStateChange={handlers.onStateChange}
+            onLoadProgress={(e: any) => handlers.onLoadProgress(e.nativeEvent.progress)}
+            onMessage={handleWebViewMessage(handlers)}
+            onShouldStartLoadWithRequest={shouldStartLoadWithRequest}
+            renderError={(errorDomain, errorCode, errorDesc) =>
+                renderWebViewError(handlers, errorDomain, errorCode, errorDesc)
+            }
+        />
+      </View>
+    );
+  }
+);
+
+export default WebViewContainer;
+
+const styles = StyleSheet.create({
+  container: { flex: 1 },
+});

+ 18 - 0
constants/settings.ts

@@ -0,0 +1,18 @@
+
+// base URL for the UNA mobile app
+export const UNA_URL = "http://hihi.com/una";
+
+// OneSignal App ID for push notifications
+export const ONESIGNAL_APP_ID = "";
+
+// usually external URLs are opened in the device browser,
+// but these exceptions are allowed to load within the WebView
+export const EXCEPTION_URLS = [
+    "redirect_uri=",
+    "redirect_uri%",
+    "redirectUri%",
+    "signin/oauth",
+    "signin%2Foauth",
+    "/m/oauth2",
+    "api.twitter.com",
+];

+ 53 - 0
constants/theme.ts

@@ -0,0 +1,53 @@
+/**
+ * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
+ * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
+ */
+
+import { Platform } from 'react-native';
+
+const tintColorLight = '#0a7ea4';
+const tintColorDark = '#fff';
+
+export const Colors = {
+  light: {
+    text: '#11181C',
+    background: '#fff',
+    tint: tintColorLight,
+    icon: '#687076',
+    tabIconDefault: '#687076',
+    tabIconSelected: tintColorLight,
+  },
+  dark: {
+    text: '#ECEDEE',
+    background: '#151718',
+    tint: tintColorDark,
+    icon: '#9BA1A6',
+    tabIconDefault: '#9BA1A6',
+    tabIconSelected: tintColorDark,
+  },
+};
+
+export const Fonts = Platform.select({
+  ios: {
+    /** iOS `UIFontDescriptorSystemDesignDefault` */
+    sans: 'system-ui',
+    /** iOS `UIFontDescriptorSystemDesignSerif` */
+    serif: 'ui-serif',
+    /** iOS `UIFontDescriptorSystemDesignRounded` */
+    rounded: 'ui-rounded',
+    /** iOS `UIFontDescriptorSystemDesignMonospaced` */
+    mono: 'ui-monospace',
+  },
+  default: {
+    sans: 'normal',
+    serif: 'serif',
+    rounded: 'normal',
+    mono: 'monospace',
+  },
+  web: {
+    sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
+    serif: "Georgia, 'Times New Roman', serif",
+    rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
+    mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
+  },
+});

+ 10 - 0
eslint.config.js

@@ -0,0 +1,10 @@
+// https://docs.expo.dev/guides/using-eslint/
+const { defineConfig } = require('eslint/config');
+const expoConfig = require('eslint-config-expo/flat');
+
+module.exports = defineConfig([
+  expoConfig,
+  {
+    ignores: ['dist/*'],
+  },
+]);

部分文件因为文件数量过多而无法显示