&
12 | Readonly<{ children?: ReactNode }>
13 | ) => {
14 | return (
15 |
16 | );
17 | };
18 |
19 | export default MaterialHeaderButton;
20 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 0.50.0"
4 | },
5 | "build": {
6 | "development": {
7 | "distribution": "internal",
8 | "android": {
9 | "gradleCommand": ":app:assembleDebug"
10 | },
11 | "ios": {
12 | "buildConfiguration": "Debug"
13 | }
14 | },
15 | "preview": {
16 | "distribution": "internal",
17 | "ios": {
18 | "simulator": true
19 | }
20 | },
21 | "production": {
22 | "android": {
23 | "image": "latest"
24 | }
25 | }
26 | },
27 | "submit": {
28 | "production": {}
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # react-native-reanimated
11 | -keep class com.swmansion.reanimated.** { *; }
12 | -keep class com.facebook.react.turbomodule.** { *; }
13 |
14 | # Add any project specific keep options here:
15 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'TamoTam'
2 |
3 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
4 | useExpoModules()
5 |
6 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
7 | applyNativeModulesSettingsGradle(settings)
8 |
9 | include ':app'
10 | includeBuild(new File(["node", "--print", "require.resolve('react-native-gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile())
11 |
--------------------------------------------------------------------------------
/android/app/src/release/java/com/tamotam/application/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.tamotam.application;
8 |
9 | import android.content.Context;
10 | import com.facebook.react.ReactInstanceManager;
11 |
12 | /**
13 | * Class responsible of loading Flipper inside your React Native application. This is the release
14 | * flavor of it so it's empty as we don't want to load Flipper.
15 | */
16 | public class ReactNativeFlipper {
17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
18 | // Do nothing as we don't want to initialize Flipper on Release.
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
14 |
17 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "TamoTam",
4 | "slug": "TamoTam",
5 | "version": "0.9.5",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "splash": {
11 | "image": "./assets/images/icon.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "updates": {
16 | "fallbackToCacheTimeout": 0
17 | },
18 | "assetBundlePatterns": ["**/*"],
19 | "ios": {
20 | "supportsTablet": true,
21 | "buildNumber": "0.9.5",
22 | "googleServicesFile": "./GoogleService-Info.plist",
23 | "bundleIdentifier": "com.tamotam.application"
24 | },
25 | "android": {
26 | "adaptiveIcon": {
27 | "foregroundImage": "./assets/images/icon.png",
28 | "backgroundColor": "#ffffff"
29 | },
30 | "googleServicesFile": "./google-services.json",
31 | "package": "com.tamotam.application",
32 | "versionCode": 95
33 | },
34 | "web": {
35 | "favicon": "./assets/images/favicon.png"
36 | },
37 | "plugins": [
38 | "@react-native-firebase/app",
39 | "@react-native-firebase/crashlytics",
40 | "@react-native-firebase/perf"
41 | ],
42 | "extra": {
43 | "eas": {
44 | "projectId": "7f793288-3c1f-458c-9255-c77e8ed3bd90"
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/common/readItemFromStorage.ts:
--------------------------------------------------------------------------------
1 | import analytics from "@react-native-firebase/analytics";
2 | import crashlytics from "@react-native-firebase/crashlytics";
3 | import AsyncStorage from '@react-native-async-storage/async-storage';
4 | import { Alert } from "react-native";
5 | import { Event } from "../interfaces/event";
6 |
7 | export const readItemFromStorage: () => Promise = async () => {
8 | let eventsParsed: Event[] = [];
9 |
10 | try {
11 | const eventsInJSONString: any = await AsyncStorage.getItem("EVENTS_ASYNC_STORAGE");
12 | eventsParsed = JSON.parse(eventsInJSONString);
13 |
14 | analytics().logEvent("custom_log", {
15 | description: "--- Analytics: common -> readItemFromStorage -> try, eventsInJSONString: " + eventsInJSONString,
16 | });
17 | } catch (error: unknown) {
18 | if (error instanceof Error) {
19 | Alert.alert(
20 | "Error ❌",
21 | "Problem with reading events from a local device.",
22 | [{ text: "Okay" }]
23 | );
24 |
25 | analytics().logEvent("custom_log", {
26 | description: "--- Analytics: common -> readItemFromStorage -> catch, error: " + error,
27 | });
28 | crashlytics().recordError(error);
29 | }
30 | } finally {
31 | analytics().logEvent("custom_log", {
32 | description: "--- Analytics: common -> readItemFromStorage -> finally",
33 | });
34 | }
35 |
36 | return eventsParsed ? eventsParsed : [];
37 | };
38 |
39 | export default readItemFromStorage;
40 |
--------------------------------------------------------------------------------
/common/writeItemToStorage.ts:
--------------------------------------------------------------------------------
1 | import analytics from "@react-native-firebase/analytics";
2 | import crashlytics from "@react-native-firebase/crashlytics";
3 | import AsyncStorage from '@react-native-async-storage/async-storage';
4 | import { Alert } from "react-native";
5 | import { Event } from "../interfaces/event";
6 |
7 | export const writeItemToStorage: (eventsToAsyncStorage: Event[]) => Promise = async (eventsToAsyncStorage: Event[]) => {
8 | const eventsInJSONString: string = JSON.stringify(eventsToAsyncStorage);
9 |
10 | try {
11 | await AsyncStorage.setItem("EVENTS_ASYNC_STORAGE", eventsInJSONString);
12 |
13 | analytics().logEvent("custom_log", {
14 | description: "--- Analytics: common -> writeItemToStorage -> try, eventsInJSONString: " + eventsInJSONString,
15 | });
16 | } catch (error: unknown) {
17 | if (error instanceof Error) {
18 | Alert.alert(
19 | "Error ❌",
20 | "Problem with saving events on a local device to avoid a big load during next application launch.",
21 | [{ text: "Okay" }]
22 | );
23 |
24 | analytics().logEvent("custom_log", {
25 | description: "--- Analytics: common -> writeItemToStorage -> catch, error: " + error,
26 | });
27 | crashlytics().recordError(error);
28 | }
29 | } finally {
30 | analytics().logEvent("custom_log", {
31 | description: "--- Analytics: common -> writeItemToStorage -> finally",
32 | });
33 | }
34 | };
35 |
36 | export default writeItemToStorage;
37 |
--------------------------------------------------------------------------------
/screens/NotFoundScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import StyledText from "../components/StyledText";
3 | import { useNetInfo, NetInfoState } from "@react-native-community/netinfo";
4 | import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
5 | import { RootStackParamList } from "../types";
6 | import { StackScreenProps } from "@react-navigation/stack";
7 |
8 | export default function NotFoundScreen({
9 | navigation,
10 | }: StackScreenProps) {
11 | const internetState: NetInfoState = useNetInfo();
12 |
13 | if (internetState.isConnected === false) {
14 | return (
15 |
16 |
17 | Please turn on the Internet to use TamoTam.
18 |
19 |
20 | );
21 | }
22 |
23 | return (
24 |
25 | This screen doesn't exist.
26 | navigation.replace("Root")}
28 | style={styles.link}
29 | >
30 | Go to home screen!
31 |
32 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | centered: {
38 | alignItems: "center",
39 | flex: 1,
40 | justifyContent: "center",
41 | },
42 | link: {
43 | marginTop: 15,
44 | paddingVertical: 15,
45 | },
46 | linkText: {
47 | color: "#2e78b7",
48 | fontSize: 14,
49 | },
50 | title: {
51 | fontSize: 20,
52 | fontWeight: "bold",
53 | textAlign: "center",
54 | },
55 | });
56 |
--------------------------------------------------------------------------------
/hooks/useCachedResources.ts:
--------------------------------------------------------------------------------
1 | import * as Font from "expo-font";
2 | import * as SplashScreen from "expo-splash-screen";
3 | import analytics from "@react-native-firebase/analytics";
4 | import crashlytics from "@react-native-firebase/crashlytics";
5 | import { useEffect, useState } from "react";
6 | import { Alert } from "react-native";
7 | import { Ionicons } from "@expo/vector-icons";
8 |
9 | export default function useCachedResources() {
10 | const [isLoadingComplete, setLoadingComplete] = useState(false);
11 |
12 | useEffect(() => {
13 | async function loadResourcesAndDataAsync() {
14 | try {
15 | SplashScreen.preventAutoHideAsync();
16 |
17 | // Load fonts
18 | await Font.loadAsync({
19 | ...Ionicons.font,
20 | "boiling-demo": require("../assets/fonts/Boiling-BlackDemo.ttf"),
21 | "space-mono": require("../assets/fonts/SpaceMono-Regular.ttf"),
22 | });
23 |
24 | analytics().logEvent("custom_log", {
25 | description: "--- Analytics: hooks -> useCachedResources -> useEffect[] -> try",
26 | });
27 | } catch (error: unknown) {
28 | if (error instanceof Error) {
29 | Alert.alert(
30 | "Error ❌",
31 | "Problem with loading a font.",
32 | [{ text: "Okay" }]
33 | );
34 |
35 | analytics().logEvent("custom_log", {
36 | description: "--- Analytics: hooks -> useCachedResources -> useEffect[] -> catch, error: " + error,
37 | });
38 | crashlytics().recordError(error);
39 | }
40 | } finally {
41 | analytics().logEvent("custom_log", {
42 | description: "--- Analytics: hooks -> useCachedResources -> useEffect[] -> finally",
43 | });
44 | setLoadingComplete(true);
45 | SplashScreen.hideAsync();
46 | }
47 | }
48 |
49 | loadResourcesAndDataAsync();
50 | }, []);
51 |
52 | return isLoadingComplete;
53 | }
54 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/components/Themed.tsx:
--------------------------------------------------------------------------------
1 | import analytics from "@react-native-firebase/analytics";
2 | import useColorScheme from "../hooks/useColorScheme";
3 | import Colors from "../constants/Colors";
4 | import React from "react";
5 | import { Text as DefaultText, View as DefaultView } from "react-native";
6 |
7 | export function useThemeColor(
8 | props: { light?: string; dark?: string },
9 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
10 | ) {
11 | const theme: "light" | "dark" = useColorScheme();
12 | const colorFromProps: string | undefined = props[theme];
13 |
14 | analytics().logEvent("custom_log", {
15 | description: "--- Analytics: components -> Themed -> useThemeColor, theme: " + theme,
16 | });
17 | if (colorFromProps) {
18 | return colorFromProps;
19 | } else {
20 | return Colors[theme][colorName];
21 | }
22 | }
23 |
24 | type ThemeProps = {
25 | darkColor?: string;
26 | lightColor?: string;
27 | };
28 |
29 | export type TextProps = ThemeProps & DefaultText["props"];
30 | export type ViewProps = ThemeProps & DefaultView["props"];
31 |
32 | // TODO: For now it's not being used, check out how text could be changed not manually, but instead using this function.
33 | export function Text(props: TextProps) {
34 | const { darkColor, lightColor, style, ...otherProps } = props;
35 | const color: string = useThemeColor({ dark: darkColor, light: lightColor }, "text");
36 |
37 | analytics().logEvent("custom_log", {
38 | description: "--- Analytics: components -> Themed -> Text, color: " + color,
39 | });
40 | return ;
41 | }
42 |
43 | export function View(props: ViewProps) {
44 | const { darkColor, lightColor, style, ...otherProps } = props;
45 | const backgroundColor: string = useThemeColor(
46 | { dark: darkColor, light: lightColor },
47 | "background"
48 | );
49 |
50 | analytics().logEvent("custom_log", {
51 | description: "--- Analytics: components -> Themed -> View, backgroundColor: " + backgroundColor,
52 | });
53 | return ;
54 | }
55 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 |
25 | # Automatically convert third-party libraries to use AndroidX
26 | android.enableJetifier=true
27 | android.disableAutomaticComponentCreation=true
28 | # Version of flipper SDK to use with React Native
29 | FLIPPER_VERSION=0.125.0
30 |
31 | # Use this property to specify which architecture you want to build.
32 | # You can also override it from the CLI using
33 | # ./gradlew -PreactNativeArchitectures=x86_64
34 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
35 |
36 | # Use this property to enable support to the new architecture.
37 | # This will allow you to use TurboModules and the Fabric render in
38 | # your application. You should enable this flag either if you want
39 | # to write custom TurboModules/Fabric components OR use libraries that
40 | # are providing them.
41 | newArchEnabled=false
42 |
43 | # The hosted JavaScript engine
44 | # Supported values: expo.jsEngine = "hermes" | "jsc"
45 | expo.jsEngine=jsc
46 |
47 | # Enable GIF support in React Native images (~200 B increase)
48 | expo.gif.enabled=true
49 | # Enable webp support in React Native images (~85 KB increase)
50 | expo.webp.enabled=true
51 | # Enable animated webp support (~3.4 MB increase)
52 | # Disabled by default because iOS doesn't support animated webp
53 | expo.webp.animated=false
54 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = findProperty('android.buildToolsVersion') ?: '33.0.0'
6 | minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21')
7 | compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '33')
8 | targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '33')
9 | if (findProperty('android.kotlinVersion')) {
10 | kotlinVersion = findProperty('android.kotlinVersion')
11 | }
12 | frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0'
13 |
14 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
15 | ndkVersion = "23.1.7779620"
16 | }
17 | repositories {
18 | google()
19 | mavenCentral()
20 |
21 | // Fix for react-native-async-storage build issue (details: https://stackoverflow.com/questions/74333132/task-react-native-async-storage-async-storagegeneratedebugrfile-failed).
22 | exclusiveContent {
23 | filter {
24 | includeGroup "com.facebook.react"
25 | }
26 | forRepository {
27 | maven {
28 | url "$rootDir/../node_modules/react-native/android"
29 | }
30 | }
31 | }
32 | }
33 | dependencies {
34 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.4'
35 | classpath 'com.google.firebase:perf-plugin:1.4.2'
36 | classpath 'com.google.gms:google-services:4.3.15'
37 | classpath('com.android.tools.build:gradle:7.4.1')
38 | classpath('com.facebook.react:react-native-gradle-plugin')
39 | }
40 | }
41 |
42 | allprojects {
43 | repositories {
44 | maven {
45 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
46 | url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android'))
47 | }
48 | maven {
49 | // Android JSC is installed from npm
50 | url(new File(['node', '--print', "require.resolve('jsc-android/package.json')"].execute(null, rootDir).text.trim(), '../dist'))
51 | }
52 |
53 | google()
54 | mavenCentral()
55 | maven { url 'https://www.jitpack.io' }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/tamotam/application/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.tamotam.application;
2 |
3 | import android.app.Application;
4 | import android.content.res.Configuration;
5 | import androidx.annotation.NonNull;
6 |
7 | import com.facebook.react.PackageList;
8 | import com.facebook.react.ReactApplication;
9 | import com.facebook.react.ReactNativeHost;
10 | import com.facebook.react.ReactPackage;
11 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
12 | import com.facebook.react.defaults.DefaultReactNativeHost;
13 | import com.facebook.soloader.SoLoader;
14 |
15 | import expo.modules.ApplicationLifecycleDispatcher;
16 | import expo.modules.ReactNativeHostWrapper;
17 |
18 | import java.util.List;
19 |
20 | public class MainApplication extends Application implements ReactApplication {
21 |
22 | private final ReactNativeHost mReactNativeHost =
23 | new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) {
24 | @Override
25 | public boolean getUseDeveloperSupport() {
26 | return BuildConfig.DEBUG;
27 | }
28 |
29 | @Override
30 | protected List getPackages() {
31 | @SuppressWarnings("UnnecessaryLocalVariable")
32 | List packages = new PackageList(this).getPackages();
33 | // Packages that cannot be autolinked yet can be added manually here, for example:
34 | // packages.add(new MyReactNativePackage());
35 | return packages;
36 | }
37 |
38 | @Override
39 | protected String getJSMainModuleName() {
40 | return "index";
41 | }
42 |
43 | @Override
44 | protected boolean isNewArchEnabled() {
45 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
46 | }
47 |
48 | @Override
49 | protected Boolean isHermesEnabled() {
50 | return BuildConfig.IS_HERMES_ENABLED;
51 | }
52 | });
53 |
54 | @Override
55 | public ReactNativeHost getReactNativeHost() {
56 | return mReactNativeHost;
57 | }
58 |
59 | @Override
60 | public void onCreate() {
61 | super.onCreate();
62 | SoLoader.init(this, /* native exopackage */ false);
63 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
64 | // If you opted-in for the New Architecture, we load the native entry point for this app.
65 | DefaultNewArchitectureEntryPoint.load();
66 | }
67 | ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
68 | ApplicationLifecycleDispatcher.onApplicationCreate(this);
69 | }
70 |
71 | @Override
72 | public void onConfigurationChanged(@NonNull Configuration newConfig) {
73 | super.onConfigurationChanged(newConfig);
74 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "start": "expo start --dev-client",
4 | "android": "expo run:android",
5 | "ios": "expo run:ios",
6 | "web": "expo start --web",
7 | "eject": "expo eject",
8 | "test": "jest --watchAll",
9 | "release:android": "react-native run-android --variant=release",
10 | "release:ios": "react-native run-ios --configuration Release"
11 | },
12 | "jest": {
13 | "preset": "jest-expo"
14 | },
15 | "dependencies": {
16 | "@expo/vector-icons": "^13.0.0",
17 | "@react-native-async-storage/async-storage": "~1.17.11",
18 | "@react-native-community/cli-platform-android": "^10.2.0",
19 | "@react-native-community/datetimepicker": "6.7.5",
20 | "@react-native-community/netinfo": "9.3.7",
21 | "@react-native-firebase/analytics": "^17.5.0",
22 | "@react-native-firebase/app": "^17.5.0",
23 | "@react-native-firebase/crashlytics": "^17.5.0",
24 | "@react-native-firebase/firestore": "^17.5.0",
25 | "@react-native-firebase/perf": "^17.5.0",
26 | "@react-native-firebase/storage": "^17.5.0",
27 | "@react-navigation/material-bottom-tabs": "^6.2.15",
28 | "@react-navigation/native": "^6.1.6",
29 | "@react-navigation/stack": "^6.3.16",
30 | "axios": "^1.3.4",
31 | "expo": "^48.0.21",
32 | "expo-asset": "~8.9.1",
33 | "expo-constants": "~14.2.1",
34 | "expo-font": "~11.1.1",
35 | "expo-image-picker": "~14.1.1",
36 | "expo-linking": "~4.0.1",
37 | "expo-location": "~15.1.1",
38 | "expo-splash-screen": "~0.18.1",
39 | "expo-sqlite": "~11.1.1",
40 | "expo-status-bar": "~1.4.2",
41 | "expo-web-browser": "~12.1.1",
42 | "react": "18.2.0",
43 | "react-dom": "18.2.0",
44 | "react-native": "0.71.3",
45 | "react-native-config": "^1.5.0",
46 | "react-native-gesture-handler": "2.9.0",
47 | "react-native-map-clustering": "^3.4.2",
48 | "react-native-maps": "^1.11.3",
49 | "react-native-paper": "^5.4.0",
50 | "react-native-reanimated": "^3.8.1",
51 | "react-native-safe-area-context": "4.5.0",
52 | "react-native-screens": "~3.20.0",
53 | "react-native-vector-icons": "^9.2.0",
54 | "react-native-web": "~0.18.12",
55 | "react-navigation-header-buttons": "^10.0.0",
56 | "react-redux": "^8.0.5",
57 | "redux": "^4.2.1",
58 | "redux-thunk": "^2.4.2"
59 | },
60 | "devDependencies": {
61 | "@babel/core": "^7.20.0",
62 | "@types/react": "^18.0.28",
63 | "@types/react-native": "~0.71.3",
64 | "@types/react-native-vector-icons": "^6.4.13",
65 | "jest-expo": "^48.0.0",
66 | "react-native-dotenv": "3.4.8",
67 | "typescript": "^4.9.4"
68 | },
69 | "private": true,
70 | "resolutions": {
71 | "@types/react": "~18.0.24"
72 | },
73 | "version": "0.9.5",
74 | "name": "TamoTam"
75 | }
76 |
--------------------------------------------------------------------------------
/store/actions/apis/predictHqEvents.tsx:
--------------------------------------------------------------------------------
1 | import analytics from '@react-native-firebase/analytics';
2 | import axios, { AxiosResponse } from 'axios';
3 | import crashlytics from '@react-native-firebase/crashlytics';
4 | import readItemFromStorage from '../../../common/readItemFromStorage';
5 | import writeItemToStorage from '../../../common/writeItemToStorage';
6 | import { Event } from '../../../interfaces/event';
7 | import {
8 | PREDICTHQ_ACCESS_TOKEN,
9 | PREDICTHQ_CATEGORIES,
10 | PREDICTHQ_LOCATIONS
11 | // @ts-ignore
12 | } from '@env';
13 |
14 | export const SET_EVENTS = 'SET_EVENTS';
15 |
16 | export const fetchPredictHqEvents: () => (dispatch: any) => void = () => {
17 | return async (dispatch: any) => {
18 | let eventsInStorage: Event[] | null | any = await readItemFromStorage();
19 |
20 | await axios({
21 | headers: {
22 | Authorization: `Bearer ${PREDICTHQ_ACCESS_TOKEN}`,
23 | Accept: 'application/json',
24 | },
25 | method: 'GET',
26 | params: {
27 | category: PREDICTHQ_CATEGORIES,
28 | "saved_location.location_id": PREDICTHQ_LOCATIONS,
29 | },
30 | url: 'https://api.predicthq.com/v1/events/',
31 | })
32 | .then((response: AxiosResponse) => {
33 | for (const id in response.data.results) {
34 | eventsInStorage.push({
35 | id: 'predicthq' + response.data.results[id].id,
36 | date: new Date(response.data.results[id].start),
37 | description: response.data.results[id].description,
38 | imageUrl: '',
39 | isUserEvent: false,
40 | latitude: response.data.results[id].location[1],
41 | longitude: response.data.results[id].location[0],
42 | title: response.data.results[id].title,
43 | });
44 | }
45 | analytics().logEvent('custom_log', {
46 | description:
47 | '--- Analytics: store -> actions -> skiRegEvents -> fetchPredictHqEvents -> try, eventsInStorage: ' +
48 | eventsInStorage,
49 | });
50 | })
51 | .catch((error: unknown) => {
52 | if (error instanceof Error) {
53 | analytics().logEvent('custom_log', {
54 | description:
55 | '--- Analytics: store -> actions -> fetchPredictHqEvents -> catch, error: ' +
56 | error,
57 | });
58 | crashlytics().recordError(error);
59 | }
60 | })
61 | .finally(() => {
62 | dispatch({
63 | type: SET_EVENTS,
64 | events: eventsInStorage,
65 | });
66 | writeItemToStorage(eventsInStorage);
67 | analytics().logEvent('custom_log', {
68 | description:
69 | '--- Analytics: store -> actions -> fetchPredictHqEvents -> finally',
70 | });
71 | });
72 | };
73 | };
74 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/tamotam/application/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.tamotam.application;
2 |
3 | import android.os.Build;
4 | import android.os.Bundle;
5 |
6 | import com.facebook.react.ReactActivity;
7 | import com.facebook.react.ReactActivityDelegate;
8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
9 | import com.facebook.react.defaults.DefaultReactActivityDelegate;
10 |
11 | import expo.modules.ReactActivityDelegateWrapper;
12 |
13 | public class MainActivity extends ReactActivity {
14 | @Override
15 | protected void onCreate(Bundle savedInstanceState) {
16 | // Set the theme to AppTheme BEFORE onCreate to support
17 | // coloring the background, status bar, and navigation bar.
18 | // This is required for expo-splash-screen.
19 | setTheme(R.style.AppTheme);
20 | super.onCreate(null);
21 | }
22 |
23 | /**
24 | * Returns the name of the main component registered from JavaScript.
25 | * This is used to schedule rendering of the component.
26 | */
27 | @Override
28 | protected String getMainComponentName() {
29 | return "main";
30 | }
31 |
32 | /**
33 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
34 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
35 | * (aka React 18) with two boolean flags.
36 | */
37 | @Override
38 | protected ReactActivityDelegate createReactActivityDelegate() {
39 | return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate(
40 | this,
41 | getMainComponentName(),
42 | // If you opted-in for the New Architecture, we enable the Fabric Renderer.
43 | DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
44 | // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
45 | DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
46 | ));
47 | }
48 |
49 | /**
50 | * Align the back button behavior with Android S
51 | * where moving root activities to background instead of finishing activities.
52 | * @see onBackPressed
53 | */
54 | @Override
55 | public void invokeDefaultOnBackPressed() {
56 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
57 | if (!moveTaskToBack(false)) {
58 | // For non-root activities, use the default implementation to finish them.
59 | super.invokeDefaultOnBackPressed();
60 | }
61 | return;
62 | }
63 |
64 | // Use the default back button implementation on Android S
65 | // because it's doing more than {@link Activity#moveTaskToBack} in fact.
66 | super.invokeDefaultOnBackPressed();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/components/EventItem.tsx:
--------------------------------------------------------------------------------
1 | import useColorScheme from "../hooks/useColorScheme";
2 | import Colors from "../constants/Colors";
3 | import React from "react";
4 | import { Avatar, Button, Card, Paragraph } from "react-native-paper";
5 |
6 | const LeftContent = (props: any) => (
7 |
18 | );
19 |
20 | const EventItem = (props: any) => {
21 | return (
22 |
30 |
38 |
39 |
47 | {props.description}
48 |
49 |
57 | 🗓️{" "}
58 | {
59 | new Date(props.date) instanceof Date
60 | ? new Date(props.date).toLocaleDateString()
61 | : "No information"
62 | }
63 |
64 |
72 | 🕒{" "}
73 | {
74 | new Date(props.date) instanceof Date
75 | ? new Date(props.date).toLocaleTimeString([], {
76 | hour: "2-digit",
77 | minute: "2-digit",
78 | })
79 | : "No information"
80 | }
81 |
82 |
83 |
90 |
91 | {props.children}
92 |
93 |
94 | );
95 | };
96 |
97 | export default EventItem;
98 |
--------------------------------------------------------------------------------
/store/actions/apis/seatGeekEvents.tsx:
--------------------------------------------------------------------------------
1 | import analytics from '@react-native-firebase/analytics';
2 | import axios, { AxiosResponse } from 'axios';
3 | import crashlytics from '@react-native-firebase/crashlytics';
4 | import readItemFromStorage from '../../../common/readItemFromStorage';
5 | import writeItemToStorage from '../../../common/writeItemToStorage';
6 | import { Dispatch } from 'redux';
7 | import { Event } from '../../../interfaces/event';
8 | import {
9 | SEATGEEK_CLIENT_ID,
10 | SEATGEEK_PAGE_SIZE,
11 | SEATGEEK_NUMBER_OF_PAGES,
12 | SEATGEEK_SECRET,
13 | // @ts-ignore
14 | } from '@env';
15 |
16 | export const SET_EVENTS = 'SET_EVENTS';
17 |
18 | export const fetchSeatGeekEvents: () => (dispatch: Dispatch) => void = () => {
19 | return async (dispatch: Dispatch) => {
20 | let eventsInStorage: Event[] | null | any = await readItemFromStorage();
21 |
22 | for (let page: number = 0; page < SEATGEEK_NUMBER_OF_PAGES; page++) {
23 | await axios({
24 | method: 'GET',
25 | url: `https://api.seatgeek.com/2/events?client_id=${SEATGEEK_CLIENT_ID}&client_secret=${SEATGEEK_SECRET}&per_page=${SEATGEEK_PAGE_SIZE}`,
26 | })
27 | .then((response: AxiosResponse) => {
28 | for (const id in response.data.events) {
29 | eventsInStorage.push({
30 | id: 'seatgeek' + response.data.events[id].id,
31 | date: new Date(response.data.events[id].datetime_local),
32 | description: response.data.events[id].description,
33 | imageUrl: response.data.events[id].performers[0].image,
34 | latitude: response.data.events[id].venue.location.lat,
35 | longitude: response.data.events[id].venue.location.lon,
36 | isUserEvent: false,
37 | title: response.data.events[id].title,
38 | });
39 | }
40 | analytics().logEvent('custom_log', {
41 | description:
42 | '--- Analytics: store -> actions -> seatGeekEvents -> fetchSeatGeekEvents -> try, eventsInStorage: ' +
43 | eventsInStorage,
44 | });
45 | })
46 | .catch((error: unknown) => {
47 | if (error instanceof Error) {
48 | analytics().logEvent('custom_log', {
49 | description:
50 | '--- Analytics: store -> actions -> seatGeekEvents -> fetchSeatGeekEvents -> catch, error: ' +
51 | error,
52 | });
53 | crashlytics().recordError(error);
54 | }
55 | })
56 | .finally(() => {
57 | analytics().logEvent('custom_log', {
58 | description:
59 | '--- Analytics: store -> actions -> seatGeekEvents -> fetchSeatGeekEvents -> finally',
60 | });
61 | });
62 | }
63 | dispatch({
64 | type: SET_EVENTS,
65 | events: eventsInStorage,
66 | });
67 | writeItemToStorage(eventsInStorage);
68 | };
69 | };
70 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/store/actions/apis/triRegEvents.tsx:
--------------------------------------------------------------------------------
1 | import analytics from '@react-native-firebase/analytics';
2 | import axios, { AxiosResponse } from 'axios';
3 | import crashlytics from '@react-native-firebase/crashlytics';
4 | import readItemFromStorage from '../../../common/readItemFromStorage';
5 | import writeItemToStorage from '../../../common/writeItemToStorage';
6 | import { Event } from '../../../interfaces/event';
7 |
8 | export const SET_EVENTS = 'SET_EVENTS';
9 |
10 | export const fetchTriRegEvents: () => (dispatch: any) => void = () => {
11 | return async (dispatch: any) => {
12 | let eventsInStorage: Event[] | null | any = await readItemFromStorage();
13 |
14 | await axios({
15 | method: 'GET',
16 | url: 'https://www.trireg.com/api/search',
17 | })
18 | .then((response: AxiosResponse) => {
19 | for (const EventId in response.data.MatchingEvents) {
20 | const arrayByDashSignDivider =
21 | response.data.MatchingEvents[EventId].EventDate.match(/\d+/g);
22 | const checkForDash = /-/.test(
23 | response.data.MatchingEvents[EventId].EventDate,
24 | )
25 | ? -1
26 | : +1;
27 | const dateInMilliseconds =
28 | +arrayByDashSignDivider[0] +
29 | checkForDash *
30 | (arrayByDashSignDivider[1].slice(0, 2) * 3.6e6 +
31 | arrayByDashSignDivider[1].slice(-2) * 6e4);
32 |
33 | eventsInStorage.push({
34 | id: 'trireg' + response.data.MatchingEvents[EventId].EventId,
35 | date: new Date(dateInMilliseconds),
36 | description: response.data.MatchingEvents[EventId].PresentedBy,
37 | imageUrl: '',
38 | isUserEvent: false,
39 | latitude: response.data.MatchingEvents[EventId].Latitude,
40 | longitude: response.data.MatchingEvents[EventId].Longitude,
41 | title: response.data.MatchingEvents[EventId].EventName,
42 | });
43 | }
44 | analytics().logEvent('custom_log', {
45 | description:
46 | '--- Analytics: store -> actions -> triRegEvents -> fetchTriRegEvents -> try, eventsInStorage: ' +
47 | eventsInStorage,
48 | });
49 | })
50 | .catch((error: unknown) => {
51 | if (error instanceof Error) {
52 | analytics().logEvent('custom_log', {
53 | description:
54 | '--- Analytics: store -> actions -> triRegEvents -> fetchTriRegEvents -> catch, error: ' +
55 | error,
56 | });
57 | crashlytics().recordError(error);
58 | }
59 | })
60 | .finally(() => {
61 | dispatch({
62 | type: SET_EVENTS,
63 | events: eventsInStorage,
64 | });
65 | writeItemToStorage(eventsInStorage);
66 | analytics().logEvent('custom_log', {
67 | description:
68 | '--- Analytics: store -> actions -> triRegEvents -> fetchTriRegEvents -> finally',
69 | });
70 | });
71 | };
72 | };
73 |
--------------------------------------------------------------------------------
/ios/TamoTam/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | TamoTam
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 0.9.5
21 | CFBundleSignature
22 | ????
23 | CFBundleURLTypes
24 |
25 |
26 | CFBundleURLSchemes
27 |
28 | myapp
29 | com.tamotam.application
30 |
31 |
32 |
33 | CFBundleURLSchemes
34 |
35 | com.googleusercontent.apps.425706026242-30a8dpl3du56blco2mdcnqtc3icq5gdf
36 |
37 |
38 |
39 | CFBundleVersion
40 | 0.9.5
41 | LSRequiresIPhoneOS
42 |
43 | NSAppTransportSecurity
44 |
45 | NSAllowsArbitraryLoads
46 |
47 | NSExceptionDomains
48 |
49 | localhost
50 |
51 | NSExceptionAllowsInsecureHTTPLoads
52 |
53 |
54 |
55 |
56 | NSCameraUsageDescription
57 | Allow $(PRODUCT_NAME) to access your camera
58 | NSLocationAlwaysAndWhenInUseUsageDescription
59 | Allow $(PRODUCT_NAME) to access your location
60 | NSLocationAlwaysUsageDescription
61 | Allow $(PRODUCT_NAME) to access your location
62 | NSLocationWhenInUseUsageDescription
63 | Allow $(PRODUCT_NAME) to access your location
64 | NSMicrophoneUsageDescription
65 | Allow $(PRODUCT_NAME) to access your microphone
66 | NSPhotoLibraryUsageDescription
67 | Allow $(PRODUCT_NAME) to access your photos
68 | UILaunchStoryboardName
69 | SplashScreen
70 | UIRequiredDeviceCapabilities
71 |
72 | armv7
73 |
74 | UIRequiresFullScreen
75 |
76 | UIStatusBarStyle
77 | UIStatusBarStyleDarkContent
78 | UISupportedInterfaceOrientations
79 |
80 | UIInterfaceOrientationPortrait
81 | UIInterfaceOrientationPortraitUpsideDown
82 |
83 | UIUserInterfaceStyle
84 | Automatic
85 | UIViewControllerBasedStatusBarAppearance
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/ios/TamoTam/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "idiom": "iphone",
5 | "size": "20x20",
6 | "scale": "2x",
7 | "filename": "App-Icon-20x20@2x.png"
8 | },
9 | {
10 | "idiom": "iphone",
11 | "size": "20x20",
12 | "scale": "3x",
13 | "filename": "App-Icon-20x20@3x.png"
14 | },
15 | {
16 | "idiom": "iphone",
17 | "size": "29x29",
18 | "scale": "1x",
19 | "filename": "App-Icon-29x29@1x.png"
20 | },
21 | {
22 | "idiom": "iphone",
23 | "size": "29x29",
24 | "scale": "2x",
25 | "filename": "App-Icon-29x29@2x.png"
26 | },
27 | {
28 | "idiom": "iphone",
29 | "size": "29x29",
30 | "scale": "3x",
31 | "filename": "App-Icon-29x29@3x.png"
32 | },
33 | {
34 | "idiom": "iphone",
35 | "size": "40x40",
36 | "scale": "2x",
37 | "filename": "App-Icon-40x40@2x.png"
38 | },
39 | {
40 | "idiom": "iphone",
41 | "size": "40x40",
42 | "scale": "3x",
43 | "filename": "App-Icon-40x40@3x.png"
44 | },
45 | {
46 | "idiom": "iphone",
47 | "size": "60x60",
48 | "scale": "2x",
49 | "filename": "App-Icon-60x60@2x.png"
50 | },
51 | {
52 | "idiom": "iphone",
53 | "size": "60x60",
54 | "scale": "3x",
55 | "filename": "App-Icon-60x60@3x.png"
56 | },
57 | {
58 | "idiom": "ipad",
59 | "size": "20x20",
60 | "scale": "1x",
61 | "filename": "App-Icon-20x20@1x.png"
62 | },
63 | {
64 | "idiom": "ipad",
65 | "size": "20x20",
66 | "scale": "2x",
67 | "filename": "App-Icon-20x20@2x.png"
68 | },
69 | {
70 | "idiom": "ipad",
71 | "size": "29x29",
72 | "scale": "1x",
73 | "filename": "App-Icon-29x29@1x.png"
74 | },
75 | {
76 | "idiom": "ipad",
77 | "size": "29x29",
78 | "scale": "2x",
79 | "filename": "App-Icon-29x29@2x.png"
80 | },
81 | {
82 | "idiom": "ipad",
83 | "size": "40x40",
84 | "scale": "1x",
85 | "filename": "App-Icon-40x40@1x.png"
86 | },
87 | {
88 | "idiom": "ipad",
89 | "size": "40x40",
90 | "scale": "2x",
91 | "filename": "App-Icon-40x40@2x.png"
92 | },
93 | {
94 | "idiom": "ipad",
95 | "size": "76x76",
96 | "scale": "1x",
97 | "filename": "App-Icon-76x76@1x.png"
98 | },
99 | {
100 | "idiom": "ipad",
101 | "size": "76x76",
102 | "scale": "2x",
103 | "filename": "App-Icon-76x76@2x.png"
104 | },
105 | {
106 | "idiom": "ipad",
107 | "size": "83.5x83.5",
108 | "scale": "2x",
109 | "filename": "App-Icon-83.5x83.5@2x.png"
110 | },
111 | {
112 | "idiom": "ios-marketing",
113 | "size": "1024x1024",
114 | "scale": "1x",
115 | "filename": "ItunesArtwork@2x.png"
116 | }
117 | ],
118 | "info": {
119 | "version": 1,
120 | "author": "expo"
121 | }
122 | }
--------------------------------------------------------------------------------
/store/reducers/events.tsx:
--------------------------------------------------------------------------------
1 | import { Event } from "../../interfaces/event";
2 | import {
3 | ADD_EVENT,
4 | DELETE_EVENT,
5 | SAVE_EVENT,
6 | SET_EVENTS,
7 | SET_SAVED_EVENTS,
8 | UPDATE_EVENT,
9 | } from "../actions/events";
10 |
11 | const initialState = {
12 | events: [],
13 | savedEvents: [],
14 | };
15 |
16 | export default (state = initialState, action: any) => {
17 | switch (action.type) {
18 | case ADD_EVENT:
19 | const newEvent: Event = {
20 | id: action.eventData.id,
21 | date: action.eventData.date,
22 | description: action.eventData.description,
23 | firestoreDocumentId: action.eventData.firestoreDocumentId,
24 | imageUrl: action.eventData.imageUrl,
25 | isUserEvent: action.eventData.isUserEvent,
26 | latitude: action.eventData.latitude,
27 | longitude: action.eventData.longitude,
28 | title: action.eventData.title,
29 | };
30 |
31 | return {
32 | ...state,
33 | // @ts-ignore
34 | events: state.events.concat(newEvent),
35 | };
36 | case DELETE_EVENT:
37 | return {
38 | ...state,
39 | savedEvents: state.savedEvents.filter(
40 | (event: Event) => event.id !== action.eventData.id
41 | ),
42 | };
43 | case SAVE_EVENT:
44 | const savedEvent: Event = {
45 | id: action.eventData.id,
46 | date: action.eventData.date,
47 | description: action.eventData.description,
48 | firestoreDocumentId: action.eventData.firestoreDocumentId,
49 | imageUrl: action.eventData.imageUrl,
50 | isUserEvent: action.eventData.isUserEvent,
51 | latitude: action.eventData.latitude,
52 | longitude: action.eventData.longitude,
53 | title: action.eventData.title,
54 | };
55 |
56 | return {
57 | ...state,
58 | // @ts-ignore
59 | savedEvents: state.savedEvents.concat(savedEvent),
60 | };
61 | case SET_EVENTS:
62 | return {
63 | ...state,
64 | events: [...state.events, ...action.events],
65 | };
66 | case SET_SAVED_EVENTS:
67 | return {
68 | ...state,
69 | savedEvents: action.savedEvents,
70 | };
71 | case UPDATE_EVENT:
72 | const eventIndex = state.savedEvents.findIndex(
73 | (event: Event) => event.id === action.eventData.id
74 | );
75 |
76 | const updatedEvent: Event = {
77 | id: action.eventData.id,
78 | date: action.eventData.date,
79 | description: action.eventData.description,
80 | firestoreDocumentId: action.eventData.firestoreDocumentId,
81 | imageUrl: action.eventData.imageUrl,
82 | isUserEvent: action.eventData.isUserEvent,
83 | latitude: action.eventData.latitude,
84 | longitude: action.eventData.longitude,
85 | title: action.eventData.title,
86 | };
87 |
88 | const updatedEditEvents: Event[] = [...state.savedEvents];
89 | updatedEditEvents[eventIndex] = updatedEvent;
90 |
91 | return {
92 | ...state,
93 | savedEvents: updatedEditEvents,
94 | };
95 | default:
96 | return state;
97 | }
98 | };
99 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if %ERRORLEVEL% equ 0 goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if %ERRORLEVEL% equ 0 goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | set EXIT_CODE=%ERRORLEVEL%
84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
86 | exit /b %EXIT_CODE%
87 |
88 | :mainEnd
89 | if "%OS%"=="Windows_NT" endlocal
90 |
91 | :omega
92 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import "react-native-gesture-handler";
2 | import analytics from "@react-native-firebase/analytics";
3 | import crashlytics from "@react-native-firebase/crashlytics";
4 | import eventsReducer from "./store/reducers/events";
5 | import useCachedResources from "./hooks/useCachedResources";
6 | import useColorScheme from "./hooks/useColorScheme";
7 | import Colors from "./constants/Colors";
8 | import React, { useEffect } from "react";
9 | import Navigation from "./navigation";
10 | import ReduxThunk from "redux-thunk";
11 | import { applyMiddleware, createStore, combineReducers } from "redux";
12 | import { init } from "./helpers/sqlite_db";
13 | import { useNetInfo, NetInfoState } from "@react-native-community/netinfo";
14 | import { Alert, StyleSheet } from "react-native";
15 | import { ColorSchemeName } from "react-native";
16 | import { StatusBar } from "expo-status-bar";
17 | import { ActivityIndicator, MD3DarkTheme, Provider as PaperProvider } from "react-native-paper";
18 | import { Provider as StoreProvider } from "react-redux";
19 | import { View } from "./components/Themed";
20 |
21 | init()
22 | .then(() => {
23 | analytics().logEvent("custom_log", {
24 | description: "--- Analytics: App -> init -> then",
25 | });
26 | })
27 | .catch((error: unknown) => {
28 | if (error instanceof Error) {
29 | analytics().logEvent("custom_log", {
30 | description: "--- Analytics: App -> init -> catch, error: " + error,
31 | });
32 | crashlytics().recordError(error);
33 | }
34 | }).finally(() => {
35 | analytics().logEvent("custom_log", {
36 | description: "--- Analytics: App -> init -> finally",
37 | });
38 | });
39 |
40 | const rootReducer = combineReducers({
41 | events: eventsReducer,
42 | });
43 |
44 | const store = createStore(rootReducer, applyMiddleware(ReduxThunk));
45 |
46 | export default function App() {
47 | const colorScheme: ColorSchemeName = useColorScheme();
48 | const internetState: NetInfoState = useNetInfo();
49 | const isLoadingComplete: boolean = useCachedResources();
50 |
51 | useEffect(() => {
52 | if (internetState.isConnected === false) {
53 | Alert.alert(
54 | "No Internet! ❌",
55 | "Sorry, we need an Internet connection for TamoTam to run correctly.",
56 | [{ text: "Okay" }]
57 | );
58 | }
59 | analytics().logEvent("custom_log", {
60 | description: "--- Analytics: App -> useEffect[internetState.isConnected]: " + internetState.isConnected,
61 | });
62 | }, [internetState.isConnected]);
63 |
64 | if (!isLoadingComplete) {
65 | return (
66 |
67 |
71 |
72 | );
73 | } else {
74 | return (
75 |
76 |
77 |
78 |
79 |
80 |
81 | );
82 | }
83 | }
84 |
85 | const styles = StyleSheet.create({
86 | centered: {
87 | alignItems: "center",
88 | flex: 1,
89 | justifyContent: "center",
90 | },
91 | });
92 |
--------------------------------------------------------------------------------
/store/actions/apis/runRegEvents.tsx:
--------------------------------------------------------------------------------
1 | import analytics from '@react-native-firebase/analytics';
2 | import axios, { AxiosResponse } from 'axios';
3 | import crashlytics from '@react-native-firebase/crashlytics';
4 | import readItemFromStorage from '../../../common/readItemFromStorage';
5 | import writeItemToStorage from '../../../common/writeItemToStorage';
6 | import { Event } from '../../../interfaces/event';
7 | import {
8 | RUNREG_NUMBER_OF_PAGES,
9 | // @ts-ignore
10 | } from '@env';
11 |
12 | export const SET_EVENTS = 'SET_EVENTS';
13 |
14 | export const fetchRunRegEvents: () => (dispatch: any) => void = () => {
15 | return async (dispatch: any) => {
16 | let eventsInStorage: Event[] | null | any = await readItemFromStorage();
17 |
18 | for (let page: number = 0; page < RUNREG_NUMBER_OF_PAGES; page++) {
19 | await axios({
20 | method: 'GET',
21 | url: `https://www.runreg.com/api/search?startpage=${page}`,
22 | })
23 | .then((response: AxiosResponse) => {
24 | for (const EventId in response.data.MatchingEvents) {
25 | const arrayByDashSignDivider =
26 | response.data.MatchingEvents[EventId].EventDate.match(/\d+/g);
27 | const checkForDash = /-/.test(
28 | response.data.MatchingEvents[EventId].EventDate,
29 | )
30 | ? -1
31 | : +1;
32 | const dateInMilliseconds =
33 | +arrayByDashSignDivider[0] +
34 | checkForDash *
35 | (arrayByDashSignDivider[1].slice(0, 2) * 3.6e6 +
36 | arrayByDashSignDivider[1].slice(-2) * 6e4);
37 |
38 | eventsInStorage.push({
39 | id: 'runreg' + response.data.MatchingEvents[EventId].EventId,
40 | date: new Date(dateInMilliseconds),
41 | description: response.data.MatchingEvents[EventId].PresentedBy,
42 | imageUrl: '',
43 | isUserEvent: false,
44 | latitude: response.data.MatchingEvents[EventId].Latitude,
45 | longitude: response.data.MatchingEvents[EventId].Longitude,
46 | title: response.data.MatchingEvents[EventId].EventName,
47 | });
48 | }
49 | analytics().logEvent('custom_log', {
50 | description:
51 | '--- Analytics: store -> actions -> runRegEvents -> fetchRunRegEvents -> try, eventsInStorage: ' +
52 | eventsInStorage,
53 | });
54 | })
55 | .catch((error: unknown) => {
56 | if (error instanceof Error) {
57 | analytics().logEvent('custom_log', {
58 | description:
59 | '--- Analytics: store -> actions -> runRegEvents -> fetchRunRegEvents -> catch, error: ' +
60 | error,
61 | });
62 | crashlytics().recordError(error);
63 | }
64 | })
65 | .finally(() => {
66 | analytics().logEvent('custom_log', {
67 | description:
68 | '--- Analytics: store -> actions -> runRegEvents -> fetchRunRegEvents -> finally',
69 | });
70 | });
71 | }
72 | dispatch({
73 | type: SET_EVENTS,
74 | events: eventsInStorage,
75 | });
76 | writeItemToStorage(eventsInStorage);
77 | };
78 | };
79 |
--------------------------------------------------------------------------------
/store/actions/apis/bikeRegEvents.tsx:
--------------------------------------------------------------------------------
1 | import analytics from '@react-native-firebase/analytics';
2 | import axios, { AxiosResponse } from 'axios';
3 | import crashlytics from '@react-native-firebase/crashlytics';
4 | import readItemFromStorage from '../../../common/readItemFromStorage';
5 | import writeItemToStorage from '../../../common/writeItemToStorage';
6 | import { Event } from '../../../interfaces/event';
7 | import {
8 | BIKEREG_NUMBER_OF_PAGES,
9 | // @ts-ignore
10 | } from '@env';
11 |
12 | export const SET_EVENTS = 'SET_EVENTS';
13 |
14 | export const fetchBikeRegEvents: () => (dispatch: any) => void = () => {
15 | return async (dispatch: any) => {
16 | let eventsInStorage: Event[] | null | any = await readItemFromStorage();
17 |
18 | for (let page: number = 0; page < BIKEREG_NUMBER_OF_PAGES; page++) {
19 | await axios({
20 | method: 'GET',
21 | url: `https://www.bikereg.com/api/search?startpage=${page}`,
22 | })
23 | .then((response: AxiosResponse) => {
24 | for (const EventId in response.data.MatchingEvents) {
25 | const arrayByDashSignDivider =
26 | response.data.MatchingEvents[EventId].EventDate.match(/\d+/g);
27 | const checkForDash = /-/.test(
28 | response.data.MatchingEvents[EventId].EventDate,
29 | )
30 | ? -1
31 | : +1;
32 | const dateInMilliseconds =
33 | +arrayByDashSignDivider[0] +
34 | checkForDash *
35 | (arrayByDashSignDivider[1].slice(0, 2) * 3.6e6 +
36 | arrayByDashSignDivider[1].slice(-2) * 6e4);
37 |
38 | eventsInStorage.push({
39 | id: 'bikereg' + response.data.MatchingEvents[EventId].EventId,
40 | date: new Date(dateInMilliseconds),
41 | description: response.data.MatchingEvents[EventId].PresentedBy,
42 | imageUrl: '',
43 | isUserEvent: false,
44 | latitude: response.data.MatchingEvents[EventId].Latitude,
45 | longitude: response.data.MatchingEvents[EventId].Longitude,
46 | title: response.data.MatchingEvents[EventId].EventName,
47 | });
48 | }
49 | analytics().logEvent('custom_log', {
50 | description:
51 | '--- Analytics: store -> actions -> bikeRegEvents -> fetchBikeRegEvents -> try, eventsInStorage: ' +
52 | eventsInStorage,
53 | });
54 | })
55 | .catch((error: unknown) => {
56 | if (error instanceof Error) {
57 | analytics().logEvent('custom_log', {
58 | description:
59 | '--- Analytics: store -> actions -> bikeRegEvents -> fetchBikeRegEvents -> catch, error: ' +
60 | error,
61 | });
62 | crashlytics().recordError(error);
63 | }
64 | })
65 | .finally(() => {
66 | analytics().logEvent('custom_log', {
67 | description:
68 | '--- Analytics: store -> actions -> bikeRegEvents -> fetchBikeRegEvents -> finally',
69 | });
70 | });
71 | }
72 | dispatch({
73 | type: SET_EVENTS,
74 | events: eventsInStorage,
75 | });
76 | writeItemToStorage(eventsInStorage);
77 | };
78 | };
79 |
--------------------------------------------------------------------------------
/store/actions/apis/skiRegEvents.tsx:
--------------------------------------------------------------------------------
1 | import analytics from '@react-native-firebase/analytics';
2 | import axios, { AxiosResponse } from 'axios';
3 | import crashlytics from '@react-native-firebase/crashlytics';
4 | import readItemFromStorage from '../../../common/readItemFromStorage';
5 | import writeItemToStorage from '../../../common/writeItemToStorage';
6 | import { Event } from '../../../interfaces/event';
7 | import {
8 | SKIREG_NUMBER_OF_PAGES,
9 | // @ts-ignore
10 | } from '@env';
11 |
12 | export const SET_EVENTS = 'SET_EVENTS';
13 |
14 | export const fetchSkiRegEvents: () => (dispatch: any) => void = () => {
15 | return async (dispatch: any) => {
16 | let eventsInStorage: Event[] | null | any = await readItemFromStorage();
17 |
18 | for (let page: number = 0; page < SKIREG_NUMBER_OF_PAGES; page++) {
19 | await axios({
20 | method: 'GET',
21 | url: `https://www.skireg.com/api/search/search?startpage=${page}`,
22 | })
23 | .then((response: AxiosResponse) => {
24 | for (const EventId in response.data.MatchingEvents) {
25 | const arrayByDashSignDivider =
26 | response.data.MatchingEvents[EventId].EventDate.match(/\d+/g);
27 | const checkForDash = /-/.test(
28 | response.data.MatchingEvents[EventId].EventDate,
29 | )
30 | ? -1
31 | : +1;
32 | const dateInMilliseconds =
33 | +arrayByDashSignDivider[0] +
34 | checkForDash *
35 | (arrayByDashSignDivider[1].slice(0, 2) * 3.6e6 +
36 | arrayByDashSignDivider[1].slice(-2) * 6e4);
37 |
38 | eventsInStorage.push({
39 | id: 'skireg' + response.data.MatchingEvents[EventId].EventId,
40 | date: new Date(dateInMilliseconds),
41 | description: response.data.MatchingEvents[EventId].PresentedBy,
42 | imageUrl: '',
43 | isUserEvent: false,
44 | latitude: response.data.MatchingEvents[EventId].Latitude,
45 | longitude: response.data.MatchingEvents[EventId].Longitude,
46 | title: response.data.MatchingEvents[EventId].EventName,
47 | });
48 | }
49 | analytics().logEvent('custom_log', {
50 | description:
51 | '--- Analytics: store -> actions -> skiRegEvents -> fetchSkiRegEvents -> try, eventsInStorage: ' +
52 | eventsInStorage,
53 | });
54 | })
55 | .catch((error: unknown) => {
56 | if (error instanceof Error) {
57 | analytics().logEvent('custom_log', {
58 | description:
59 | '--- Analytics: store -> actions -> skiRegEvents -> fetchSkiRegEvents -> catch, error: ' +
60 | error,
61 | });
62 | crashlytics().recordError(error);
63 | }
64 | })
65 | .finally(() => {
66 | dispatch({
67 | type: SET_EVENTS,
68 | events: eventsInStorage,
69 | });
70 | writeItemToStorage(eventsInStorage);
71 | analytics().logEvent('custom_log', {
72 | description:
73 | '--- Analytics: store -> actions -> skiRegEvents -> fetchSkiRegEvents -> finally',
74 | });
75 | });
76 | }
77 | };
78 | };
79 |
--------------------------------------------------------------------------------
/navigation/index.tsx:
--------------------------------------------------------------------------------
1 | import analytics from "@react-native-firebase/analytics";
2 | import BottomTabNavigator from "./BottomTabNavigator";
3 | import EditEventScreen from "../screens/EditEventScreen";
4 | import EventDetailScreen from "../screens/EventDetailScreen";
5 | import LinkingConfiguration from "./LinkingConfiguration";
6 | import NewEventScreen from "../screens/NewEventScreen";
7 | import NotFoundScreen from "../screens/NotFoundScreen";
8 | import React, { useRef } from "react";
9 | import { createStackNavigator } from "@react-navigation/stack";
10 | import { ColorSchemeName } from "react-native";
11 | import {
12 | DarkTheme,
13 | DefaultTheme,
14 | NavigationContainer,
15 | } from "@react-navigation/native";
16 | import { RootStackParamList } from "../types";
17 |
18 | export default function Navigation({
19 | colorScheme,
20 | }: {
21 | colorScheme: ColorSchemeName;
22 | }) {
23 | const routeNameRef: React.MutableRefObject = useRef();
24 | const navigationRef: any = useRef();
25 |
26 | return (
27 | {
31 | routeNameRef.current = navigationRef.current.getCurrentRoute().name;
32 | }}
33 | onStateChange={async () => {
34 | const currentRouteName: any = navigationRef.current.getCurrentRoute().name;
35 | const previousRouteName: any = routeNameRef.current;
36 |
37 | if (previousRouteName !== currentRouteName) {
38 | await analytics().logScreenView({
39 | screen_name: currentRouteName,
40 | screen_class: currentRouteName,
41 | });
42 | }
43 | routeNameRef.current = currentRouteName;
44 | }}
45 | theme={colorScheme === "dark" ? DarkTheme : DefaultTheme}
46 | >
47 |
48 |
49 | );
50 | }
51 |
52 | const Stack = createStackNavigator();
53 | function RootNavigator() {
54 | return (
55 |
56 |
63 |
73 |
83 |
93 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/ios/TamoTam.xcodeproj/xcshareddata/xcschemes/TamoTam.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/android/app/src/debug/java/com/tamotam/application/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.tamotam.application;
8 |
9 | import android.content.Context;
10 | import com.facebook.flipper.android.AndroidFlipperClient;
11 | import com.facebook.flipper.android.utils.FlipperUtils;
12 | import com.facebook.flipper.core.FlipperClient;
13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping;
17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
20 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
21 | import com.facebook.react.ReactInstanceEventListener;
22 | import com.facebook.react.ReactInstanceManager;
23 | import com.facebook.react.bridge.ReactContext;
24 | import com.facebook.react.modules.network.NetworkingModule;
25 | import okhttp3.OkHttpClient;
26 |
27 | /**
28 | * Class responsible of loading Flipper inside your React Native application. This is the debug
29 | * flavor of it. Here you can add your own plugins and customize the Flipper setup.
30 | */
31 | public class ReactNativeFlipper {
32 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
33 | if (FlipperUtils.shouldEnableFlipper(context)) {
34 | final FlipperClient client = AndroidFlipperClient.getInstance(context);
35 |
36 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
37 | client.addPlugin(new DatabasesFlipperPlugin(context));
38 | client.addPlugin(new SharedPreferencesFlipperPlugin(context));
39 | client.addPlugin(CrashReporterPlugin.getInstance());
40 |
41 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
42 | NetworkingModule.setCustomClientBuilder(
43 | new NetworkingModule.CustomClientBuilder() {
44 | @Override
45 | public void apply(OkHttpClient.Builder builder) {
46 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
47 | }
48 | });
49 | client.addPlugin(networkFlipperPlugin);
50 | client.start();
51 |
52 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
53 | // Hence we run if after all native modules have been initialized
54 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
55 | if (reactContext == null) {
56 | reactInstanceManager.addReactInstanceEventListener(
57 | new ReactInstanceEventListener() {
58 | @Override
59 | public void onReactContextInitialized(ReactContext reactContext) {
60 | reactInstanceManager.removeReactInstanceEventListener(this);
61 | reactContext.runOnNativeModulesQueueThread(
62 | new Runnable() {
63 | @Override
64 | public void run() {
65 | client.addPlugin(new FrescoFlipperPlugin());
66 | }
67 | });
68 | }
69 | });
70 | } else {
71 | client.addPlugin(new FrescoFlipperPlugin());
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/ios/TamoTam/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #import "RNCConfig.h"
8 | #import
9 |
10 | @implementation AppDelegate
11 |
12 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
13 | {
14 | NSString *mapsApiKey = [RNCConfig envFor:@"GOOGLE_MAPS_API"];
15 | [GMSServices provideAPIKey:mapsApiKey];
16 |
17 | // @generated begin @react-native-firebase/app-didFinishLaunchingWithOptions-fallback - expo prebuild (DO NOT MODIFY) sync-ecd111c37e49fdd1ed6354203cd6b1e2a38cccda
18 | [FIRApp configure];
19 | // @generated end @react-native-firebase/app-didFinishLaunchingWithOptions
20 | self.moduleName = @"main";
21 |
22 | // You can add your custom initial props in the dictionary below.
23 | // They will be passed down to the ViewController used by React Native.
24 | self.initialProps = @{};
25 |
26 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
27 | }
28 |
29 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
30 | {
31 | #if DEBUG
32 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
33 | #else
34 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
35 | #endif
36 | }
37 |
38 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
39 | ///
40 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html
41 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture).
42 | /// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`.
43 | - (BOOL)concurrentRootEnabled
44 | {
45 | return true;
46 | }
47 |
48 | // Linking API
49 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options {
50 | return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
51 | }
52 |
53 | // Universal Links
54 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler {
55 | BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
56 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result;
57 | }
58 |
59 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
60 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
61 | {
62 | return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
63 | }
64 |
65 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
66 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
67 | {
68 | return [super application:application didFailToRegisterForRemoteNotificationsWithError:error];
69 | }
70 |
71 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries
72 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
73 | {
74 | return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
75 | }
76 |
77 | @end
78 |
--------------------------------------------------------------------------------
/store/actions/apis/usersEvents.tsx:
--------------------------------------------------------------------------------
1 | import analytics from '@react-native-firebase/analytics';
2 | import crashlytics from '@react-native-firebase/crashlytics';
3 | import firestore from '@react-native-firebase/firestore';
4 | import readItemFromStorage from '../../../common/readItemFromStorage';
5 | import storage from "@react-native-firebase/storage";
6 | import writeItemToStorage from '../../../common/writeItemToStorage';
7 | import { Event } from '../../../interfaces/event';
8 | import {
9 | FIRESTORE_COLLECTION,
10 | // @ts-ignore
11 | } from '@env';
12 |
13 | export const SET_EVENTS = 'SET_EVENTS';
14 |
15 | export const fetchUsersEvents: () => (dispatch: any) => void = () => {
16 | return async (dispatch: any) => {
17 | let eventsInStorage: Event[] | null | any = await readItemFromStorage();
18 | let imageUrlPath: string = "";
19 |
20 | await firestore()
21 | .collection(FIRESTORE_COLLECTION)
22 | .get()
23 | .then(querySnapshot => {
24 | querySnapshot.forEach(async documentSnapshot => {
25 | await storage().ref(documentSnapshot.data().imageUrl).getDownloadURL()
26 | .then((imageUrlStorage) => {
27 | imageUrlPath = imageUrlStorage;
28 |
29 | analytics().logEvent('custom_log', {
30 | description:
31 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> then -> then2'
32 | });
33 | }).catch((error: unknown) => {
34 | if (error instanceof Error) {
35 | imageUrlPath = "";
36 |
37 | analytics().logEvent('custom_log', {
38 | description:
39 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> then -> catch, error: ' +
40 | error,
41 | });
42 | crashlytics().recordError(error);
43 | }
44 | });
45 |
46 | eventsInStorage.push({
47 | id: documentSnapshot.data().id,
48 | date: new Date(documentSnapshot.data().date.seconds * 1000),
49 | description: documentSnapshot.data().description,
50 | // @ts-ignore
51 | firestoreDocumentId: documentSnapshot.ref._documentPath._parts[1],
52 | imageUrl: imageUrlPath,
53 | isUserEvent: documentSnapshot.data().isUserEvent,
54 | latitude: documentSnapshot.data().latitude,
55 | longitude: documentSnapshot.data().longitude,
56 | title: documentSnapshot.data().title,
57 | });
58 | });
59 | analytics().logEvent('custom_log', {
60 | description:
61 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> then, eventsInStorage: ' +
62 | eventsInStorage,
63 | });
64 | })
65 | .catch((error: unknown) => {
66 | if (error instanceof Error) {
67 | analytics().logEvent('custom_log', {
68 | description:
69 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> catch, error: ' +
70 | error,
71 | });
72 | crashlytics().recordError(error);
73 | }
74 | })
75 | .finally(() => {
76 | // TODO: Temporary solution with the "setTimeout", make it properly with async actions (redux-thunk).
77 | setTimeout(() => {
78 | dispatch({
79 | type: SET_EVENTS,
80 | events: eventsInStorage,
81 | });
82 | writeItemToStorage(eventsInStorage);
83 | }, 1000);
84 | analytics().logEvent('custom_log', {
85 | description:
86 | '--- Analytics: store -> actions -> usersEvents -> fetchUsersEvents -> finally',
87 | });
88 | });
89 | };
90 | };
91 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 | .env.production
76 |
77 | # parcel-bundler cache (https://parceljs.org/)
78 | .cache
79 | .parcel-cache
80 |
81 | # Next.js build output
82 | .next
83 | out
84 |
85 | # Nuxt.js build / generate output
86 | .nuxt
87 | dist
88 |
89 | # Gatsby files
90 | .cache/
91 | # Comment in the public line in if your project uses Gatsby and not Next.js
92 | # https://nextjs.org/blog/next-9-1#public-directory-support
93 | # public
94 |
95 | # vuepress build output
96 | .vuepress/dist
97 |
98 | # Serverless directories
99 | .serverless/
100 |
101 | # FuseBox cache
102 | .fusebox/
103 |
104 | # DynamoDB Local files
105 | .dynamodb/
106 |
107 | # TernJS port file
108 | .tern-port
109 |
110 | # Stores VSCode versions used for testing VSCode extensions
111 | .vscode-test
112 |
113 | # yarn v2
114 | .yarn/cache
115 | .yarn/unplugged
116 | .yarn/build-state.yml
117 | .yarn/install-state.gz
118 | .pnp.*
119 |
120 | # OSX
121 | #
122 | .DS_Store
123 |
124 | # React Native
125 | .expo/
126 | *.jks
127 | *.p8
128 | *.p12
129 | *.key
130 | *.mobileprovision
131 | *.orig.*
132 | web-build/
133 |
134 | # Others
135 | google-services.json
136 | GoogleService-Info.plist
137 |
138 |
139 |
140 | # @generated expo-cli sync-647791c5bd841d5c91864afb91c302553d300921
141 | # The following patterns were generated by expo-cli
142 |
143 | # OSX
144 | #
145 | .DS_Store
146 |
147 | # Xcode
148 | #
149 | build/
150 | *.pbxuser
151 | !default.pbxuser
152 | *.mode1v3
153 | !default.mode1v3
154 | *.mode2v3
155 | !default.mode2v3
156 | *.perspectivev3
157 | !default.perspectivev3
158 | xcuserdata
159 | *.xccheckout
160 | *.moved-aside
161 | DerivedData
162 | *.hmap
163 | *.ipa
164 | *.xcuserstate
165 | project.xcworkspace
166 |
167 | # Android/IntelliJ
168 | #
169 | build/
170 | .idea
171 | .gradle
172 | local.properties
173 | *.iml
174 | *.hprof
175 | .cxx/
176 | *.keystore
177 | !debug.keystore
178 | release
179 |
180 | # node.js
181 | #
182 | node_modules/
183 | npm-debug.log
184 | yarn-error.log
185 |
186 | # Bundle artifacts
187 | *.jsbundle
188 |
189 | # CocoaPods
190 | /ios/Pods/
191 |
192 | # Temporary files created by Metro to check the health of the file watcher
193 | .metro-health-check*
194 |
195 | # Expo
196 | .expo/
197 | web-build/
198 | dist/
199 |
200 | # @end expo-cli
201 |
--------------------------------------------------------------------------------
/store/actions/apis/ticketmasterEvents.tsx:
--------------------------------------------------------------------------------
1 | import analytics from '@react-native-firebase/analytics';
2 | import axios, { AxiosResponse } from 'axios';
3 | import crashlytics from '@react-native-firebase/crashlytics';
4 | import readItemFromStorage from '../../../common/readItemFromStorage';
5 | import writeItemToStorage from '../../../common/writeItemToStorage';
6 | import {
7 | TICKETMASTER_API_KEY,
8 | TICKETMASTER_NUMBER_OF_PAGES,
9 | TICKETMASTER_SIZE,
10 | // @ts-ignore
11 | } from '@env';
12 | import { Event } from '../../../interfaces/event';
13 |
14 | export const SET_EVENTS = 'SET_EVENTS';
15 |
16 | export const fetchTicketmasterEvents: () => (dispatch: any) => void = () => {
17 | return async (dispatch: any) => {
18 | let eventsInStorage: Event[] | null | any = await readItemFromStorage();
19 | let ticketmasterEventsArrayFromSet: Event[] = [];
20 |
21 | const ticketmasterCountries: string[] = [
22 | 'AT',
23 | 'AU',
24 | 'BE',
25 | 'CA',
26 | 'CH',
27 | 'CZ',
28 | 'DE',
29 | 'DK',
30 | 'ES',
31 | 'FI',
32 | 'GB',
33 | 'IE',
34 | 'LU',
35 | 'MX',
36 | 'NO',
37 | 'NL',
38 | 'PL',
39 | 'PT',
40 | 'SE',
41 | 'US',
42 | ];
43 | for (const country of ticketmasterCountries) {
44 | for (let page: number = 0; page < TICKETMASTER_NUMBER_OF_PAGES; page++) {
45 | await axios({
46 | method: 'GET',
47 | url: `https://app.ticketmaster.com/discovery/v2/events.json?countryCode=${country}&apikey=${TICKETMASTER_API_KEY}&size=${TICKETMASTER_SIZE}&page=${page}`,
48 | })
49 | .then((response: AxiosResponse) => {
50 | for (const id in response.data._embedded.events) {
51 | eventsInStorage.push({
52 | id: 'ticketmaster' + response.data._embedded.events[id].id,
53 | date: new Date(
54 | response.data._embedded.events[id].dates.start.dateTime,
55 | ),
56 | description:
57 | response.data._embedded.events[id]._embedded.venues[0].city
58 | .name,
59 | imageUrl: response.data._embedded.events[id].images[0].url,
60 | isUserEvent: false,
61 | latitude:
62 | response.data._embedded.events[id]._embedded.venues[0]
63 | .location.latitude,
64 | longitude:
65 | response.data._embedded.events[id]._embedded.venues[0]
66 | .location.longitude,
67 | title: response.data._embedded.events[id].name,
68 | });
69 | }
70 | analytics().logEvent('custom_log', {
71 | description:
72 | '--- Analytics: store -> actions -> ticketmasterEvents -> fetchTicketmasterEvents -> try, eventsInStorage: ' +
73 | eventsInStorage,
74 | });
75 | })
76 | .catch((error: unknown) => {
77 | if (error instanceof Error) {
78 | analytics().logEvent('custom_log', {
79 | description:
80 | '--- Analytics: store -> actions -> ticketmasterEvents -> fetchTicketmasterEvents -> catch, error: ' +
81 | error,
82 | });
83 | crashlytics().recordError(error);
84 | }
85 | })
86 | .finally(() => {
87 | analytics().logEvent('custom_log', {
88 | description:
89 | '--- Analytics: store -> actions -> ticketmasterEvents -> fetchTicketmasterEvents -> finally',
90 | });
91 | });
92 | }
93 | }
94 | ticketmasterEventsArrayFromSet = Array.from(new Set(eventsInStorage));
95 | dispatch({
96 | type: SET_EVENTS,
97 | events: ticketmasterEventsArrayFromSet,
98 | });
99 | writeItemToStorage(ticketmasterEventsArrayFromSet);
100 | };
101 | };
102 |
--------------------------------------------------------------------------------
/components/SelectImage.tsx:
--------------------------------------------------------------------------------
1 | import * as ImagePicker from "expo-image-picker";
2 | import analytics from "@react-native-firebase/analytics";
3 | import storage, { FirebaseStorageTypes } from "@react-native-firebase/storage";
4 | import useColorScheme from "../hooks/useColorScheme";
5 | import Colors from "../constants/Colors";
6 | import React, { useEffect, useState } from "react";
7 | import { ColorSchemeName, Image, Platform, StyleSheet } from "react-native";
8 | import { Button } from "react-native-paper";
9 | import { Text, View } from "./Themed";
10 |
11 | const SelectImage = (props: {
12 | existingImageUrl?: string;
13 | imageUrlStorageFromChild: (arg0: string) => void;
14 | }) => {
15 | const colorScheme: ColorSchemeName = useColorScheme();
16 | const [pickedImage, setPickedImage] = useState("");
17 |
18 | useEffect(() => {
19 | (async () => {
20 | if (Platform.OS !== "web") {
21 | const { status } =
22 | await ImagePicker.requestMediaLibraryPermissionsAsync();
23 |
24 | analytics().logEvent("custom_log", {
25 | description: "--- Analytics: components -> SelectImage -> useEffect[], status: " + status,
26 | });
27 | analytics().logEvent("custom_log", {
28 | description: "--- Analytics: components -> SelectImage -> useEffect[], Platform.OS: " + Platform.OS,
29 | });
30 | }
31 | })();
32 | }, []);
33 |
34 | const selectImageHandler = async () => {
35 | let bucketStorageReference: FirebaseStorageTypes.Reference | string = "";
36 | let image: ImagePicker.ImagePickerResult =
37 | await ImagePicker.launchImageLibraryAsync({
38 | // TODO: Shall it be also camera?
39 | mediaTypes: ImagePicker.MediaTypeOptions.All,
40 | allowsEditing: true,
41 | aspect: [16, 9],
42 | quality: 1,
43 | });
44 |
45 | if (!image.canceled) {
46 | setPickedImage(image.assets[0].uri);
47 |
48 | bucketStorageReference = await storage().ref(new Date().getTime().toString()); // Randomize the file name of an image in order not to forward local path of a user image.
49 |
50 | const pathToFile: string = image.assets[0].uri;
51 | const bucketStorageTaskSnapshot: FirebaseStorageTypes.TaskSnapshot | any = await bucketStorageReference.putFile(pathToFile);
52 |
53 | props.imageUrlStorageFromChild(bucketStorageTaskSnapshot.metadata.fullPath);
54 | }
55 | analytics().logEvent("custom_log", {
56 | description: "--- Analytics: components -> SelectImage -> selectImageHandler, image: " + image,
57 | });
58 | };
59 |
60 | return (
61 |
62 |
63 | {!pickedImage &&
64 | (!props.existingImageUrl || typeof props.existingImageUrl !== "string") ? (
65 | No image picked yet.
66 | ) : (
67 |
75 | )}
76 |
77 |
91 |
92 | );
93 | };
94 |
95 | const styles = StyleSheet.create({
96 | selectImage: {
97 | alignItems: "center",
98 | marginBottom: 15,
99 | marginTop: 15,
100 | },
101 | imagePreview: {
102 | alignItems: "center",
103 | borderColor: "#ccc",
104 | borderRadius: 10,
105 | borderWidth: 1,
106 | height: 200,
107 | justifyContent: "center",
108 | marginBottom: 10,
109 | width: "100%",
110 | },
111 | image: {
112 | borderRadius: 10,
113 | height: "100%",
114 | width: "100%",
115 | },
116 | });
117 |
118 | export default SelectImage;
119 |
--------------------------------------------------------------------------------
/navigation/BottomTabNavigator.tsx:
--------------------------------------------------------------------------------
1 | import useColorScheme from "../hooks/useColorScheme";
2 | import Colors from "../constants/Colors";
3 | import MapScreen from "../screens/MapScreen";
4 | import React, { Fragment } from "react";
5 | import SavedScreen from "../screens/SavedScreen";
6 | import TabBarIcon from "../components/TabBarIcon";
7 | import { createStackNavigator } from "@react-navigation/stack";
8 | import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
9 | import { useIsFocused } from "@react-navigation/native";
10 | import { BottomTabParamList, MapParamList, SavedParamList } from "../types";
11 | import { ColorSchemeName } from "react-native";
12 | import { FAB, Portal } from "react-native-paper";
13 |
14 | const BottomTab = createMaterialBottomTabNavigator();
15 |
16 | export default function BottomTabNavigator({ navigation }: any) {
17 | const colorScheme: ColorSchemeName = useColorScheme();
18 | const isFocused: boolean = useIsFocused();
19 |
20 | return (
21 |
22 |
39 | {
44 | let iconName: string = !focused
45 | ? "map-check"
46 | : "map-check-outline";
47 |
48 | return ;
49 | },
50 | }}
51 | />
52 | {
57 | let iconName: string = !focused
58 | ? "bookmark"
59 | : "bookmark-check-outline";
60 |
61 | return ;
62 | },
63 | }}
64 | />
65 |
66 |
67 | navigation.navigate("NewEvent")}
72 | style={{
73 | backgroundColor:
74 | colorScheme === "dark"
75 | ? Colors.dark.background
76 | : Colors.light.background,
77 | borderWidth: 1,
78 | borderColor:
79 | colorScheme === "dark" ? Colors.dark.text : Colors.light.text,
80 | borderRadius: 50,
81 | bottom: 125,
82 | position: "absolute",
83 | right: 10,
84 | shadowColor:
85 | colorScheme === "dark" ? Colors.dark.text : Colors.light.text,
86 | shadowRadius: 15,
87 | }}
88 | visible={isFocused}
89 | />
90 |
91 |
92 | );
93 | }
94 |
95 | const MapStack = createStackNavigator();
96 | function MapNavigator() {
97 | return (
98 |
99 |
109 |
110 | );
111 | }
112 |
113 | const SavedStack = createStackNavigator();
114 | function SavedNavigator() {
115 | return (
116 |
117 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
3 | require File.join(File.dirname(`node --print "require.resolve('@react-native-community/cli-platform-ios/package.json')"`), "native_modules")
4 |
5 | require 'json'
6 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
7 |
8 | ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
9 |
10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '13.4'
11 | install! 'cocoapods',
12 | :deterministic_uuids => false
13 |
14 | prepare_react_native_project!
15 |
16 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.
17 | # because `react-native-flipper` depends on (FlipperKit,...), which will be excluded. To fix this,
18 | # you can also exclude `react-native-flipper` in `react-native.config.js`
19 | #
20 | # ```js
21 | # module.exports = {
22 | # dependencies: {
23 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}),
24 | # }
25 | # }
26 | # ```
27 | flipper_config = FlipperConfiguration.disabled
28 | if ENV['NO_FLIPPER'] == '1' then
29 | # Explicitly disabled through environment variables
30 | flipper_config = FlipperConfiguration.disabled
31 | elsif podfile_properties.key?('ios.flipper') then
32 | # Configure Flipper in Podfile.properties.json
33 | if podfile_properties['ios.flipper'] == 'true' then
34 | flipper_config = FlipperConfiguration.enabled(["Debug", "Release"])
35 | elsif podfile_properties['ios.flipper'] != 'false' then
36 | flipper_config = FlipperConfiguration.enabled(["Debug", "Release"], { 'Flipper' => podfile_properties['ios.flipper'] })
37 | end
38 | end
39 |
40 | target 'TamoTam' do
41 | use_expo_modules!
42 |
43 | pod 'react-native-google-maps', :path => '../node_modules/react-native-maps'
44 | pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage'
45 | pod 'Firebase', :modular_headers => true
46 | pod 'FirebaseAppCheckInterop', :modular_headers => true
47 | pod 'FirebaseAuthInterop', :modular_headers => true
48 | pod 'FirebaseCore', :modular_headers => true
49 | pod 'FirebaseCoreExtension', :modular_headers => true
50 | pod 'FirebaseInstallations', :modular_headers => true
51 | pod 'GoogleDataTransport', :modular_headers => true
52 | pod 'GTMSessionFetcher', :modular_headers => true
53 | pod 'nanopb', :modular_headers => true
54 | pod 'GoogleUtilities', :modular_headers => true
55 | use_frameworks! :linkage => :static
56 | $RNFirebaseAsStaticFramework = true
57 | config = use_native_modules!
58 |
59 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
60 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
61 |
62 | # Flags change depending on the env values.
63 | flags = get_default_flags()
64 |
65 | use_react_native!(
66 | :path => config[:reactNativePath],
67 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
68 | :fabric_enabled => flags[:fabric_enabled],
69 | # An absolute path to your application root.
70 | :app_path => "#{Pod::Config.instance.installation_root}/..",
71 | # Note that if you have use_frameworks! enabled, Flipper will not work if enabled
72 | :flipper_configuration => flipper_config
73 | )
74 |
75 | post_install do |installer|
76 | react_native_post_install(
77 | installer,
78 | config[:reactNativePath],
79 | # Set `mac_catalyst_enabled` to `true` in order to apply patches
80 | # necessary for Mac Catalyst builds
81 | :mac_catalyst_enabled => false
82 | )
83 | __apply_Xcode_12_5_M1_post_install_workaround(installer)
84 |
85 | installer.pods_project.build_configurations.each do |config|
86 | config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
87 | end
88 |
89 | # This is necessary for Xcode 14, because it signs resource bundles by default
90 | # when building for devices.
91 | installer.target_installation_results.pod_target_installation_results
92 | .each do |pod_name, target_installation_result|
93 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
94 | resource_bundle_target.build_configurations.each do |config|
95 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
96 | end
97 | end
98 | end
99 | end
100 |
101 | post_integrate do |installer|
102 | begin
103 | expo_patch_react_imports!(installer)
104 | rescue => e
105 | Pod::UI.warn e
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/ios/TamoTam/SplashScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/screens/EventDetailScreen.tsx:
--------------------------------------------------------------------------------
1 | import useColorScheme from "../hooks/useColorScheme";
2 | import Colors from "../constants/Colors";
3 | import CustomMapStyles from "../constants/CustomMapStyles";
4 | import MapView, { Marker, PROVIDER_GOOGLE } from "react-native-maps";
5 | import MaterialHeaderButton from "../components/MaterialHeaderButton";
6 | import React, { useLayoutEffect, useRef, MutableRefObject, useEffect } from "react";
7 | import StyledText from "../components/StyledText";
8 | import { useNetInfo, NetInfoState } from "@react-native-community/netinfo";
9 | import { useSelector } from "react-redux";
10 | import { ColorSchemeName, Dimensions, Image, ScrollView, StyleSheet } from "react-native";
11 | import { Coordinate } from "../interfaces/coordinate";
12 | import { Event } from "../interfaces/event";
13 | import { HeaderButtons, Item } from "react-navigation-header-buttons";
14 | import { Region } from "../interfaces/region";
15 | import { View } from "../components/Themed";
16 |
17 | export default function EventDetailScreen({ navigation, route }: any) {
18 | const colorScheme: ColorSchemeName = useColorScheme();
19 | const eventId: number = route.params.eventId;
20 | const internetState: NetInfoState = useNetInfo();
21 | const mapRef: MutableRefObject = useRef(null);
22 | const selectedEvent: Event = useSelector((state: any) =>
23 | state.events.savedEvents.find((event: Event) => event.id === eventId)
24 | );
25 | const initialRegionValue: Region = {
26 | latitude: selectedEvent.latitude,
27 | longitude: selectedEvent.longitude,
28 | latitudeDelta: 10,
29 | longitudeDelta: 10,
30 | };
31 | let markerCoordinates: Coordinate = {
32 | latitude: selectedEvent.latitude ? selectedEvent.latitude : 0,
33 | longitude: selectedEvent.longitude ? selectedEvent.longitude : 0,
34 | };
35 |
36 | useLayoutEffect(() => {
37 | navigation.setOptions({
38 | headerLeft: () => (
39 |
40 | - navigation.goBack()}
48 | title="back"
49 | />
50 |
51 | ),
52 | });
53 | }, [navigation]);
54 |
55 | if (internetState.isConnected === false) {
56 | return (
57 |
58 |
59 | Please turn on the Internet to use TamoTam.
60 |
61 |
62 | );
63 | }
64 |
65 | const Map: () => JSX.Element = () => (
66 |
67 |
76 | {markerCoordinates && (
77 |
83 | )}
84 |
85 |
86 | );
87 |
88 | return (
89 |
90 |
91 | {selectedEvent.title}
92 |
93 | {selectedEvent.description}
94 |
95 |
96 | 🗓️{" "}
97 | {
98 | new Date(selectedEvent.date) instanceof Date
99 | ? new Date(selectedEvent.date).toLocaleDateString()
100 | : "No information"
101 | }
102 |
103 |
104 | 🕒{" "}
105 | {
106 | new Date(selectedEvent.date) instanceof Date
107 | ? new Date(selectedEvent.date).toLocaleTimeString([], {
108 | hour: "2-digit",
109 | minute: "2-digit",
110 | })
111 | : "No information"
112 | }
113 |
114 |
122 | {!selectedEvent.latitude || !selectedEvent.longitude ?
123 |
124 |
125 | Problem with obtaining coordinates.
126 |
127 | :
128 | }
129 |
130 |
131 | );
132 | }
133 |
134 | const styles = StyleSheet.create({
135 | centered: {
136 | alignItems: "center",
137 | flex: 1,
138 | justifyContent: "center",
139 | },
140 | container: {
141 | alignItems: "center",
142 | flex: 1,
143 | justifyContent: "center",
144 | marginHorizontal: 30,
145 | },
146 | description: {
147 | marginBottom: 10,
148 | },
149 | image: {
150 | alignItems: "center",
151 | borderColor: "#ccc",
152 | borderRadius: 10,
153 | borderWidth: 1,
154 | height: 200,
155 | justifyContent: "center",
156 | width: "100%",
157 | },
158 | map: {
159 | height: Dimensions.get("window").height / 2,
160 | marginTop: 30,
161 | width: Dimensions.get("window").width,
162 | },
163 | title: {
164 | fontSize: 20,
165 | fontWeight: "bold",
166 | textAlign: "center",
167 | },
168 | });
169 |
--------------------------------------------------------------------------------
/constants/CustomMapStyles.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | CUSTOM_MAP_STYLES: [
3 | {
4 | elementType: "geometry",
5 | stylers: [
6 | {
7 | color: "#ebe3cd",
8 | },
9 | ],
10 | },
11 | {
12 | elementType: "labels.text.fill",
13 | stylers: [
14 | {
15 | color: "#523735",
16 | },
17 | ],
18 | },
19 | {
20 | elementType: "labels.text.stroke",
21 | stylers: [
22 | {
23 | color: "#f5f1e6",
24 | },
25 | ],
26 | },
27 | {
28 | featureType: "administrative",
29 | elementType: "geometry.stroke",
30 | stylers: [
31 | {
32 | color: "#c9b2a6",
33 | },
34 | ],
35 | },
36 | {
37 | featureType: "administrative.land_parcel",
38 | elementType: "geometry.stroke",
39 | stylers: [
40 | {
41 | color: "#dcd2be",
42 | },
43 | ],
44 | },
45 | {
46 | featureType: "administrative.land_parcel",
47 | elementType: "labels.text.fill",
48 | stylers: [
49 | {
50 | color: "#ae9e90",
51 | },
52 | ],
53 | },
54 | {
55 | featureType: "landscape.natural",
56 | elementType: "geometry",
57 | stylers: [
58 | {
59 | color: "#dfd2ae",
60 | },
61 | ],
62 | },
63 | {
64 | featureType: "poi",
65 | elementType: "geometry",
66 | stylers: [
67 | {
68 | color: "#dfd2ae",
69 | },
70 | ],
71 | },
72 | {
73 | featureType: "poi",
74 | elementType: "labels.text.fill",
75 | stylers: [
76 | {
77 | color: "#93817c",
78 | },
79 | ],
80 | },
81 | {
82 | featureType: "poi.business",
83 | elementType: "labels.icon",
84 | stylers: [
85 | {
86 | visibility: "off",
87 | },
88 | ],
89 | },
90 | {
91 | featureType: "poi.business",
92 | elementType: "labels.text.fill",
93 | stylers: [
94 | {
95 | visibility: "off",
96 | },
97 | ],
98 | },
99 | {
100 | featureType: "poi.medical",
101 | elementType: "geometry.fill",
102 | stylers: [
103 | {
104 | color: "#feecf9",
105 | },
106 | ],
107 | },
108 | {
109 | featureType: "poi.medical",
110 | elementType: "labels.icon",
111 | stylers: [
112 | {
113 | visibility: "off",
114 | },
115 | ],
116 | },
117 | {
118 | featureType: "poi.medical",
119 | elementType: "labels.text",
120 | stylers: [
121 | {
122 | visibility: "off",
123 | },
124 | ],
125 | },
126 | {
127 | featureType: "poi.park",
128 | elementType: "geometry.fill",
129 | stylers: [
130 | {
131 | color: "#a5b076",
132 | },
133 | ],
134 | },
135 | {
136 | featureType: "poi.park",
137 | elementType: "labels.text.fill",
138 | stylers: [
139 | {
140 | color: "#447530",
141 | },
142 | ],
143 | },
144 | {
145 | featureType: "road",
146 | elementType: "geometry",
147 | stylers: [
148 | {
149 | color: "#f5f1e6",
150 | },
151 | ],
152 | },
153 | {
154 | featureType: "road.arterial",
155 | elementType: "geometry",
156 | stylers: [
157 | {
158 | color: "#fdfcf8",
159 | },
160 | ],
161 | },
162 | {
163 | featureType: "road.highway",
164 | elementType: "geometry",
165 | stylers: [
166 | {
167 | color: "#f8c967",
168 | },
169 | ],
170 | },
171 | {
172 | featureType: "road.highway",
173 | elementType: "geometry.stroke",
174 | stylers: [
175 | {
176 | color: "#e9bc62",
177 | },
178 | ],
179 | },
180 | {
181 | featureType: "road.highway.controlled_access",
182 | elementType: "geometry",
183 | stylers: [
184 | {
185 | color: "#e98d58",
186 | },
187 | ],
188 | },
189 | {
190 | featureType: "road.highway.controlled_access",
191 | elementType: "geometry.stroke",
192 | stylers: [
193 | {
194 | color: "#db8555",
195 | },
196 | ],
197 | },
198 | {
199 | featureType: "road.local",
200 | elementType: "labels.text.fill",
201 | stylers: [
202 | {
203 | color: "#806b63",
204 | },
205 | {
206 | visibility: "off",
207 | },
208 | ],
209 | },
210 | {
211 | featureType: "transit",
212 | elementType: "geometry.fill",
213 | stylers: [
214 | {
215 | color: "#000000",
216 | },
217 | {
218 | visibility: "off",
219 | },
220 | ],
221 | },
222 | {
223 | featureType: "transit.line",
224 | elementType: "geometry",
225 | stylers: [
226 | {
227 | color: "#dfd2ae",
228 | },
229 | ],
230 | },
231 | {
232 | featureType: "transit.line",
233 | elementType: "labels.text.fill",
234 | stylers: [
235 | {
236 | color: "#8f7d77",
237 | },
238 | ],
239 | },
240 | {
241 | featureType: "transit.line",
242 | elementType: "labels.text.stroke",
243 | stylers: [
244 | {
245 | color: "#ebe3cd",
246 | },
247 | ],
248 | },
249 | {
250 | featureType: "transit.station",
251 | elementType: "geometry",
252 | stylers: [
253 | {
254 | color: "#dfd2ae",
255 | },
256 | ],
257 | },
258 | {
259 | featureType: "water",
260 | elementType: "geometry.fill",
261 | stylers: [
262 | {
263 | color: "#b9d3c2",
264 | },
265 | ],
266 | },
267 | {
268 | featureType: "water",
269 | elementType: "labels.text.fill",
270 | stylers: [
271 | {
272 | color: "#92998d",
273 | },
274 | ],
275 | },
276 | ],
277 | };
278 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🤙 TamoTam. HangOut. Offline.
2 |
3 | [](https://apps.apple.com/pl/app/tamotam-hangout-offline/id1625649957)
4 | [](https://play.google.com/store/apps/details?id=com.tamotam.application)
5 |
6 | ## Description
7 |
8 | Map with offline events happening around you. You don't need to log in or register.
9 |
10 | ### Go Online to be Offline.
11 |
12 | TamoTam aims to limit online time spent on digital applications in favor of offline social life outside the screen.
13 |
14 | The application aims to be minimalistic and exclude features such as:
15 |
16 | - Addictive Feed,
17 | - Destructive Notifications,
18 | - Brain-affecting Likes,
19 | - Comments, #StopHate,
20 | - Share, #StopFakeNews,
21 | - Other complicated Algorithms unconsciously affect your stay online.
22 |
23 | That's because we believe Social Media affects us mentally, such as Anxiety, Depression, Fear Of Missing Out (FOMO), Fear of Speaking Up, Isolation, and more.
24 |
25 | Look what's happening around you in Real. Offline. Social Life.
26 | Could you add your events (log in or registration required) to contribute to that idea?
27 |
28 | ### Note
29 |
30 | The application is in an early stage of development with a number of [improvements & issues](https://github.com/tamotam-com/tamotam-app/issues).
31 | It will take time before the application will be complete, performant and user-friendly, but in late 2023 we expect Early Beta.
32 |
33 | Thanks in advance for understanding while reviewing TamoTam.
34 |
35 | ## Developers
36 |
37 | That's an Open Source project; feel free to contribute.
38 |
39 | ### Frontend/JavaScript/Web Developer interested in React Native/JavaScript Mobile Development?
40 |
41 | The project is a combination of a personal education project to learn JavaScript Mobile Development using `React Native` and business idea to create a mobile app with offline-only events to limit online time.
42 |
43 | Therefore, even if that's just from business point of view super easy app for now, it contains several nice technical implementation, which helped me to understand the ecosystem on `React Native` and become better developer.
44 |
45 | ### Technological stack
46 |
47 | - `React/Native` + `TypeScript`;
48 | - `Redux`;
49 | - Different databases (`Firebase`, `SQLite`, `AsyncStorage`);
50 | - `AsyncStorage`, which is really comparable to `localStorage` we have on Web;
51 | - Caching data using `AsyncStorage`;
52 | - Making the Web code actually compiled & working with deployed app to the store;
53 | - Logging & Monitoring to `Firebase Analytics`/`Firebase Performance Monitoring`;
54 | - Reporting crashes to `Firebase Crashlytics`;
55 | - Handling environmental variables in `Android`, `iOS`, and `Web` - changes in native code were required;
56 | - Integration of 3rd party tools, like `Firebase`-* and `Google Maps`;
57 | - Usage, looks & feel of `Material Design` (`React Native Paper`) in a real application.
58 |
59 | ### Stay up to date
60 |
61 | Keep yourself up to date about TamoTam and me motivated by giving a **Star** :-)
62 | 
63 |
64 | ## Run it
65 |
66 | ### Android Simulator
67 |
68 | 1. Run on `Android Studio`
69 | 2. `yarn start`
70 | 3. `adb reverse tcp:8081 tcp:8081`
71 |
72 | Alternatively, `expo run:android --variant release`, for production version.
73 |
74 | #### Kill Android Simulator
75 |
76 | `adb -s emulator-5554 emu kill`, where `emulator-5554` is the emulator name.
77 |
78 | ### iOS Simulator
79 |
80 | 1. Build using `Xcode`, if the application isn't installed on the simulator
81 | 2. `yarn start`
82 | 3. `i`
83 |
84 | Alternatively, `expo run:ios --configuration Release`, for production version.
85 |
86 | ### Release
87 |
88 | 1. `eas build -p android`
89 | 2. `eas build -p ios`
90 |
91 | ## Architecture
92 |
93 | We're using `Redux`, but the easiest to understand the architecture is the image below with `Flux` architecture, which in fact is really similar. However, it's important to note we're using 1 store, like in `Redux` architecture.
94 |
95 | 
96 | *Image source: https://www.freecodecamp.org/news/an-introduction-to-the-flux-architectural-pattern-674ea74775c9/*
97 |
98 | External API's, like `Ticketmaster`, provide +/- 10k of external events. The `TamoTam`'s client is fetching also events added by the user, from `Firebase`. After all events will be fetched, those are being cached locally, using [AsyncStorage](https://github.com/react-native-async-storage/async-storage). After events are being cached locally on a device, the user can save their favourite events. Those saved events are also saved locally on a device. Those are saved using `SQLite`. In addition to that, users can add their own events, and those are saved in `Firebase`.
99 |
100 | 
101 | *Made using: https://app.diagrams.net*
102 |
103 | ## In media
104 |
105 | - [Codrops Newsletter](https://tympanus.net/codrops/collective/collective-736/)
106 | - [React Status Newsletter](https://react.statuscode.com/issues/310)
107 | - [Reddit r/reactnative](https://www.reddit.com/r/reactnative/comments/xzcxn8/react_native_typescript_app_with_firebase/)
108 |
109 | ## Research
110 |
111 | - [Brain Drain: The Mere Presence of One’s Own Smartphone Reduces Available Cognitive Capacity](https://www.journals.uchicago.edu/doi/full/10.1086/691462)
112 | - [No More FOMO: Limiting Social Media Decreases Loneliness and Depression](https://guilfordjournals.com/doi/10.1521/jscp.2018.37.10.751)
113 | - [Increases in Depression, Self‐Harm, and Suicide Among U.S. Adolescents After 2012 and Links to Technology Use: Possible Mechanisms](https://prcp.psychiatryonline.org/doi/full/10.1176/appi.prcp.20190015)
114 | - [A systematic review: the influence of social media on depression, anxiety and psychological distress in adolescents](https://www.tandfonline.com/doi/full/10.1080/02673843.2019.1590851)
115 | - [Cognitive Effects of Social Media Use: A Case of Older Adults](https://journals.sagepub.com/doi/full/10.1177/2056305118787203)
116 | - [Associations between smartphone use and mental health and well-being among young Swiss men](https://www.sciencedirect.com/science/article/pii/S002239562200588X)
117 | - [Excessive Smartphone Use Is Associated With Health Problems in Adolescents and Young Adults](https://www.frontiersin.org/articles/10.3389/fpsyt.2021.669042/full)
118 |
119 | ## Contact
120 |
121 | contact[at]tamotam[dot]com
122 |
123 | (demo) number of Android downloads badge: [https://playbadges.pavi2410.me/badge/full?id=com.tamotam.application](https://playbadges.pavi2410.me/badge/full?id=com.tamotam.application)
124 |
--------------------------------------------------------------------------------
/helpers/sqlite_db.ts:
--------------------------------------------------------------------------------
1 | import * as SQLite from "expo-sqlite";
2 | import analytics from "@react-native-firebase/analytics";
3 | import crashlytics from "@react-native-firebase/crashlytics";
4 | import { SQLError, SQLResultSet, SQLTransaction, WebSQLDatabase } from "expo-sqlite";
5 |
6 | const sqlite_db: WebSQLDatabase = SQLite.openDatabase("savedEvents.db");
7 |
8 | export const init: () => Promise = () => {
9 | const promise: Promise = new Promise((resolve: (value: unknown) => void, reject: (reason?: any) => void) => {
10 | sqlite_db.transaction((SQLiteTransaction: SQLTransaction) => {
11 | SQLiteTransaction.executeSql(
12 | `CREATE TABLE IF NOT EXISTS savedEvents (id INTEGER PRIMARY KEY NOT NULL, date REAL NOT NULL, description TEXT, firestoreDocumentId TEXT, imageUrl TEXT, isUserEvent INTEGER NOT NULL, latitude REAL NOT NULL, longitude REAL NOT NULL, title TEXT)`,
13 | [],
14 | (transaction: SQLTransaction, result: SQLResultSet) => {
15 | resolve(result);
16 | analytics().logEvent("custom_log", {
17 | description: "--- Analytics: helpers -> sqlite_db -> init -> (transaction, result), transaction: " + transaction,
18 | });
19 | analytics().logEvent("custom_log", {
20 | description: "--- Analytics: helpers -> sqlite_db -> init -> (transaction, result), result: " + result,
21 | });
22 | },
23 | (transaction: SQLTransaction, error: SQLError | any): void | any => {
24 | reject(error);
25 | analytics().logEvent("custom_log", {
26 | description: "--- Analytics: helpers -> sqlite_db -> init -> (transaction, error), transaction: " + transaction,
27 | });
28 | analytics().logEvent("custom_log", {
29 | description: "--- Analytics: helpers -> sqlite_db -> init -> (transaction, error), error: " + error,
30 | });
31 | crashlytics().recordError(error);
32 | }
33 | );
34 | });
35 | });
36 |
37 | return promise;
38 | };
39 |
40 | export const deleteSavedEvent: (id: number | string) => Promise = (
41 | id: number | string
42 | ) => {
43 | const promise: Promise = new Promise((resolve: (value: unknown) => void, reject: (reason?: any) => void) => {
44 | sqlite_db.transaction((SQLiteTransaction: SQLTransaction) => {
45 | SQLiteTransaction.executeSql(`DELETE FROM savedEvents WHERE id = ?;`, [id],
46 | (transaction: SQLTransaction, result: SQLResultSet) => {
47 | resolve(result);
48 | analytics().logEvent("custom_log", {
49 | description: "--- Analytics: helpers -> sqlite_db -> deleteSavedEvent -> (transaction, result), transaction: " + transaction,
50 | });
51 | analytics().logEvent("custom_log", {
52 | description: "--- Analytics: helpers -> sqlite_db -> deleteSavedEvent -> (transaction, result), result: " + result,
53 | });
54 | },
55 | (transaction: SQLTransaction, error: SQLError | any): void | any => {
56 | reject(error);
57 | analytics().logEvent("custom_log", {
58 | description: "--- Analytics: helpers -> sqlite_db -> deleteSavedEvent -> (transaction, error), transaction: " + transaction,
59 | });
60 | analytics().logEvent("custom_log", {
61 | description: "--- Analytics: helpers -> sqlite_db -> deleteSavedEvent -> (transaction, error), error: " + error,
62 | });
63 | crashlytics().recordError(error);
64 | })
65 | })
66 | });
67 |
68 | return promise;
69 | };
70 |
71 | export const insertSavedEvent:
72 | (date: Date, description: string, firestoreDocumentId: string | undefined, imageUrl: string, isUserEvent: boolean, latitude: number, longitude: number, title: string) => Promise = (
73 | date: Date,
74 | description: string,
75 | firestoreDocumentId: string | undefined,
76 | imageUrl: string,
77 | isUserEvent: boolean,
78 | latitude: number,
79 | longitude: number,
80 | title: string
81 | ) => {
82 | const promise: Promise = new Promise((resolve: (value: unknown) => void, reject: (reason?: any) => void) => {
83 | sqlite_db.transaction((SQLiteTransaction: SQLTransaction) => {
84 | SQLiteTransaction.executeSql(
85 | `INSERT INTO savedEvents (date, description, firestoreDocumentId, imageUrl, isUserEvent, latitude, longitude, title) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
86 | [String(date), description, firestoreDocumentId ? firestoreDocumentId : null, imageUrl, Number(isUserEvent), latitude, longitude, title],
87 | (transaction: SQLTransaction, result: SQLResultSet) => {
88 | resolve(result);
89 | analytics().logEvent("custom_log", {
90 | description: "--- Analytics: helpers -> sqlite_db -> insertSavedEvent -> (transaction, result), transaction: " + transaction,
91 | });
92 | analytics().logEvent("custom_log", {
93 | description: "--- Analytics: helpers -> sqlite_db -> insertSavedEvent -> (transaction, result), result: " + result,
94 | });
95 | },
96 | (transaction: SQLTransaction, error: SQLError | any): void | any => {
97 | reject(error);
98 | analytics().logEvent("custom_log", {
99 | description: "--- Analytics: helpers -> sqlite_db -> insertSavedEvent -> (transaction, error), transaction: " + transaction,
100 | });
101 | analytics().logEvent("custom_log", {
102 | description: "--- Analytics: helpers -> sqlite_db -> insertSavedEvent -> (transaction, error), error: " + error,
103 | });
104 | crashlytics().recordError(error);
105 | }
106 | );
107 | });
108 | });
109 |
110 | return promise;
111 | };
112 |
113 | export const fetchSavedEvents: () => Promise = () => {
114 | const promise: Promise = new Promise((resolve: (value: unknown) => void, reject: (reason?: any) => void) => {
115 | sqlite_db.transaction((SQLiteTransaction: SQLTransaction) => {
116 | SQLiteTransaction.executeSql(
117 | "SELECT * FROM savedEvents",
118 | [],
119 | (transaction: SQLTransaction, result: SQLResultSet) => {
120 | resolve(result);
121 | analytics().logEvent("custom_log", {
122 | description: "--- Analytics: helpers -> sqlite_db -> fetchSavedEvents -> (transaction, result), transaction: " + transaction,
123 | });
124 | analytics().logEvent("custom_log", {
125 | description: "--- Analytics: helpers -> sqlite_db -> fetchSavedEvents -> (transaction, result), result: " + result,
126 | });
127 | },
128 | (transaction: SQLTransaction, error: SQLError | any): void | any => {
129 | reject(error);
130 | analytics().logEvent("custom_log", {
131 | description: "--- Analytics: helpers -> sqlite_db -> fetchSavedEvents -> (transaction, error), transaction: " + transaction,
132 | });
133 | analytics().logEvent("custom_log", {
134 | description: "--- Analytics: helpers -> sqlite_db -> fetchSavedEvents -> (transaction, error), error: " + error,
135 | });
136 | crashlytics().recordError(error);
137 | }
138 | );
139 | });
140 | });
141 |
142 | return promise;
143 | };
144 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Stop when "xargs" is not available.
209 | if ! command -v xargs >/dev/null 2>&1
210 | then
211 | die "xargs is not available"
212 | fi
213 |
214 | # Use "xargs" to parse quoted args.
215 | #
216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
217 | #
218 | # In Bash we could simply go:
219 | #
220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
221 | # set -- "${ARGS[@]}" "$@"
222 | #
223 | # but POSIX shell has neither arrays nor command substitution, so instead we
224 | # post-process each arg (as a line of input to sed) to backslash-escape any
225 | # character that might be a shell metacharacter, then use eval to reverse
226 | # that process (while maintaining the separation between arguments), and wrap
227 | # the whole thing up as a single "set" statement.
228 | #
229 | # This will of course break if any of these variables contains a newline or
230 | # an unmatched quote.
231 | #
232 |
233 | eval "set -- $(
234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
235 | xargs -n1 |
236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
237 | tr '\n' ' '
238 | )" '"$@"'
239 |
240 | exec "$JAVACMD" "$@"
241 |
--------------------------------------------------------------------------------
/screens/SavedScreen.tsx:
--------------------------------------------------------------------------------
1 | import analytics from "@react-native-firebase/analytics";
2 | import crashlytics from "@react-native-firebase/crashlytics";
3 | import useColorScheme from "../hooks/useColorScheme";
4 | import Colors from "../constants/Colors";
5 | import EventItem from "../components/EventItem";
6 | import MaterialHeaderButton from "../components/MaterialHeaderButton";
7 | import React, { useCallback, useEffect, useLayoutEffect, useState, Dispatch } from "react";
8 | import StyledText from "../components/StyledText";
9 | import { deleteEvent } from "../store/actions/events";
10 | import { fetchUsersSavedEvents } from "../store/actions/events";
11 | import { useDispatch, useSelector } from "react-redux";
12 | import { useNetInfo, NetInfoState } from "@react-native-community/netinfo";
13 | import { ActivityIndicator, Alert, ColorSchemeName, FlatList, StyleSheet } from "react-native";
14 | import { Button } from "react-native-paper";
15 | import { Event } from "../interfaces/event";
16 | import { HeaderButtons, Item } from "react-navigation-header-buttons";
17 | import { View } from "../components/Themed";
18 |
19 | export default function SavedScreen({ navigation, route }: any) {
20 | const colorScheme: ColorSchemeName = useColorScheme();
21 | const dispatch: Dispatch = useDispatch>();
22 | const internetState: NetInfoState = useNetInfo();
23 | const savedEvents: Event[] = useSelector(
24 | (state: any) => state.events.savedEvents
25 | );
26 | const [error, setError] = useState(new Error());
27 | const [isLoading, setIsLoading] = useState(false);
28 |
29 | useEffect(() => {
30 | if (error.message !== "") {
31 | Alert.alert(
32 | "Unknown Error ❌",
33 | "Please report this error by sending an email to us at feedback@tamotam.com. It will help us 🙏\nError details: " + error.message + "\nDate: " + new Date(),
34 | [{ text: "Okay" }]
35 | );
36 | analytics().logEvent("custom_log", {
37 | description: "--- Analytics: screens -> SavedScreen -> useEffect[error], error: " + error,
38 | });
39 | crashlytics().recordError(error);
40 | }
41 | }, [error]);
42 |
43 | useLayoutEffect(() => {
44 | navigation.setOptions({
45 | headerLeft: () => (
46 |
47 | - navigation.goBack()}
55 | title="back"
56 | />
57 |
58 | ),
59 | });
60 | }, [navigation]);
61 |
62 | const loadSavedEvents: () => Promise = useCallback(async () => {
63 | analytics().logEvent("custom_log", {
64 | description: "--- Analytics: screens -> SavedScreen -> loadSavedEvents",
65 | });
66 | setError(new Error());
67 | setIsLoading(true);
68 |
69 | try {
70 | analytics().logEvent("custom_log", {
71 | description: "--- Analytics: screens -> SavedScreen -> loadSavedEvents -> try",
72 | });
73 | dispatch(fetchUsersSavedEvents());
74 | } catch (error: unknown) {
75 | if (error instanceof Error) {
76 | Alert.alert(
77 | "Error ❌",
78 | "We couldn't load saved events, sorry.\nTry to reload TamoTam!",
79 | [{ text: "Okay" }]
80 | );
81 |
82 | analytics().logEvent("custom_log", {
83 | description: "--- Analytics: screens -> SavedScreen -> loadSavedEvents -> catch, error: " + error,
84 | });
85 | crashlytics().recordError(error);
86 | setError(new Error(error.message));
87 | }
88 | } finally {
89 | analytics().logEvent("custom_log", {
90 | description: "--- Analytics: screens -> SavedScreen -> loadSavedEvents -> finally",
91 | });
92 | setIsLoading(false);
93 | }
94 | }, [dispatch, setError, setIsLoading]);
95 |
96 | useEffect(() => {
97 | analytics().logEvent("custom_log", {
98 | description: "--- Analytics: screens -> SavedScreen -> useEffect[loadSavedEvents]",
99 | });
100 | loadSavedEvents();
101 | }, [loadSavedEvents]);
102 |
103 | if (isLoading) {
104 | return (
105 |
106 |
110 |
111 | );
112 | }
113 |
114 | if (internetState.isConnected === true && (savedEvents.length === 0 || !savedEvents)) {
115 | return (
116 |
117 |
118 | No saved events found. Start adding some!
119 |
120 |
121 | );
122 | }
123 |
124 | if (internetState.isConnected === false) {
125 | return (
126 |
127 |
128 | Please turn on the Internet to use TamoTam.
129 |
130 |
131 | );
132 | }
133 |
134 | const deleteHandler: (event: Event) => void = (event: Event) => {
135 | analytics().logEvent("custom_log", {
136 | description: "--- Analytics: screens -> SavedScreen -> deleteHandler",
137 | });
138 | Alert.alert("⚠️ Delete saved event ⚠️", "Do you want to perform this irreversible deletion?", [
139 | { text: "No", style: "default" },
140 | {
141 | text: "Yes",
142 | style: "destructive",
143 | onPress: () => {
144 | setError(new Error());
145 | setIsLoading(true);
146 |
147 | try {
148 | analytics().logEvent("custom_log", {
149 | description: "--- Analytics: screens -> SavedScreen -> deleteHandler -> try, event: " + event,
150 | });
151 | dispatch(deleteEvent(event));
152 | } catch (error: unknown) {
153 | if (error instanceof Error) {
154 | Alert.alert(
155 | "Error ❌",
156 | "TamoTam couldn't save this event.\nTry one more time!",
157 | [{ text: "Okay" }]
158 | );
159 |
160 | analytics().logEvent("custom_log", {
161 | description: "--- Analytics: screens -> SavedScreen -> deleteHandler -> catch, error: " + error,
162 | });
163 | crashlytics().recordError(error);
164 | setError(new Error(error.message));
165 | }
166 | } finally {
167 | analytics().logEvent("custom_log", {
168 | description: "--- Analytics: screens -> SavedScreen -> deleteHandler -> finally",
169 | });
170 | setIsLoading(false);
171 | }
172 | },
173 | },
174 | ]);
175 | };
176 |
177 | return (
178 | index.toString()}
181 | renderItem={(eventData: Event | any) => (
182 |
193 |
212 |
230 |
245 |
246 | )}
247 | />
248 | );
249 | }
250 |
251 | const styles = StyleSheet.create({
252 | centered: {
253 | alignItems: "center",
254 | flex: 1,
255 | justifyContent: "center",
256 | },
257 | title: {
258 | fontSize: 20,
259 | fontWeight: "bold",
260 | textAlign: "center",
261 | },
262 | });
263 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 | apply plugin: "com.facebook.react"
3 |
4 | import com.android.build.OutputFile
5 | apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
6 |
7 |
8 | def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
9 | def expoDebuggableVariants = ['debug']
10 | // Override `debuggableVariants` for expo-updates debugging
11 | if (System.getenv('EX_UPDATES_NATIVE_DEBUG') == "1") {
12 | react {
13 | expoDebuggableVariants = []
14 | }
15 | }
16 |
17 |
18 | /**
19 | * This is the configuration block to customize your React Native Android app.
20 | * By default you don't need to apply any configuration, just uncomment the lines you need.
21 | */
22 | react {
23 | entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
24 | reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
25 | hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
26 | debuggableVariants = expoDebuggableVariants
27 |
28 | /* Folders */
29 | // The root of your project, i.e. where "package.json" lives. Default is '..'
30 | // root = file("../")
31 | // The folder where the react-native NPM package is. Default is ../node_modules/react-native
32 | // reactNativeDir = file("../node_modules/react-native")
33 | // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
34 | // codegenDir = file("../node_modules/react-native-codegen")
35 | // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
36 | // cliFile = file("../node_modules/react-native/cli.js")
37 |
38 | /* Variants */
39 | // The list of variants to that are debuggable. For those we're going to
40 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'.
41 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
42 | // debuggableVariants = ["liteDebug", "prodDebug"]
43 |
44 | /* Bundling */
45 | // A list containing the node command and its flags. Default is just 'node'.
46 | // nodeExecutableAndArgs = ["node"]
47 | //
48 | // The command to run when bundling. By default is 'bundle'
49 | // bundleCommand = "ram-bundle"
50 | //
51 | // The path to the CLI configuration file. Default is empty.
52 | // bundleConfig = file(../rn-cli.config.js)
53 | //
54 | // The name of the generated asset file containing your JS bundle
55 | // bundleAssetName = "MyApplication.android.bundle"
56 | //
57 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
58 | // entryFile = file("../js/MyApplication.android.js")
59 | //
60 | // A list of extra flags to pass to the 'bundle' commands.
61 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
62 | // extraPackagerArgs = []
63 |
64 | /* Hermes Commands */
65 | // The hermes compiler command to run. By default it is 'hermesc'
66 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
67 | //
68 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
69 | // hermesFlags = ["-O", "-output-source-map"]
70 | }
71 |
72 | // Override `hermesEnabled` by `expo.jsEngine`
73 | ext {
74 | hermesEnabled = (findProperty('expo.jsEngine') ?: "hermes") == "hermes"
75 | }
76 |
77 | /**
78 | * Set this to true to create four separate APKs instead of one,
79 | * one for each native architecture. This is useful if you don't
80 | * use App Bundles (https://developer.android.com/guide/app-bundle/)
81 | * and want to have separate APKs to upload to the Play Store.
82 | */
83 | def enableSeparateBuildPerCPUArchitecture = false
84 |
85 | /**
86 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
87 | */
88 | def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
89 |
90 | /**
91 | * The preferred build flavor of JavaScriptCore (JSC)
92 | *
93 | * For example, to use the international variant, you can use:
94 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
95 | *
96 | * The international variant includes ICU i18n library and necessary data
97 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
98 | * give correct results when using with locales other than en-US. Note that
99 | * this variant is about 6MiB larger per architecture than default.
100 | */
101 | def jscFlavor = 'org.webkit:android-jsc:+'
102 |
103 | /**
104 | * Private function to get the list of Native Architectures you want to build.
105 | * This reads the value from reactNativeArchitectures in your gradle.properties
106 | * file and works together with the --active-arch-only flag of react-native run-android.
107 | */
108 | def reactNativeArchitectures() {
109 | def value = project.getProperties().get("reactNativeArchitectures")
110 | return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
111 | }
112 |
113 | android {
114 | ndkVersion rootProject.ext.ndkVersion
115 |
116 | compileSdkVersion rootProject.ext.compileSdkVersion
117 |
118 | namespace "com.tamotam.application"
119 | defaultConfig {
120 | applicationId 'com.tamotam.application'
121 | minSdkVersion rootProject.ext.minSdkVersion
122 | resValue "string", "build_config_package", "com.tamotam.application"
123 | targetSdkVersion rootProject.ext.targetSdkVersion
124 | versionCode 95
125 | versionName "0.9.5"
126 | }
127 |
128 | splits {
129 | abi {
130 | reset()
131 | enable enableSeparateBuildPerCPUArchitecture
132 | universalApk false // If true, also generate a universal APK
133 | include (*reactNativeArchitectures())
134 | }
135 | }
136 | signingConfigs {
137 | debug {
138 | storeFile file('debug.keystore')
139 | storePassword 'android'
140 | keyAlias 'androiddebugkey'
141 | keyPassword 'android'
142 | }
143 | }
144 | buildTypes {
145 | debug {
146 | signingConfig signingConfigs.debug
147 | }
148 | release {
149 | // Caution! In production, you need to generate your own keystore file.
150 | // see https://reactnative.dev/docs/signed-apk-android.
151 | signingConfig signingConfigs.debug
152 | minifyEnabled enableProguardInReleaseBuilds
153 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
154 | }
155 | }
156 |
157 | // applicationVariants are e.g. debug, release
158 | applicationVariants.all { variant ->
159 | variant.outputs.each { output ->
160 | // For each separate APK per architecture, set a unique version code as described here:
161 | // https://developer.android.com/studio/build/configure-apk-splits.html
162 | // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
163 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
164 | def abi = output.getFilter(OutputFile.ABI)
165 | if (abi != null) { // null for the universal-debug, universal-release variants
166 | output.versionCodeOverride =
167 | defaultConfig.versionCode * 1000 + versionCodes.get(abi)
168 | }
169 |
170 | }
171 | }
172 | }
173 |
174 | // Apply static values from `gradle.properties` to the `android.packagingOptions`
175 | // Accepts values in comma delimited lists, example:
176 | // android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
177 | ["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
178 | // Split option: 'foo,bar' -> ['foo', 'bar']
179 | def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
180 | // Trim all elements in place.
181 | for (i in 0.. 0) {
186 | println "android.packagingOptions.$prop += $options ($options.length)"
187 | // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
188 | options.each {
189 | android.packagingOptions[prop] += it
190 | }
191 | }
192 | }
193 |
194 | dependencies {
195 | // The version of react-native is set by the React Native Gradle Plugin
196 | implementation("com.facebook.react:react-android")
197 |
198 | def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
199 | def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
200 | def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
201 | def frescoVersion = rootProject.ext.frescoVersion
202 |
203 | // If your app supports Android versions before Ice Cream Sandwich (API level 14)
204 | if (isGifEnabled || isWebpEnabled) {
205 | implementation("com.facebook.fresco:fresco:${frescoVersion}")
206 | implementation("com.facebook.fresco:imagepipeline-okhttp3:${frescoVersion}")
207 | }
208 |
209 | if (isGifEnabled) {
210 | // For animated gif support
211 | implementation("com.facebook.fresco:animated-gif:${frescoVersion}")
212 | }
213 |
214 | if (isWebpEnabled) {
215 | // For webp support
216 | implementation("com.facebook.fresco:webpsupport:${frescoVersion}")
217 | if (isWebpAnimatedEnabled) {
218 | // Animated webp support
219 | implementation("com.facebook.fresco:animated-webp:${frescoVersion}")
220 | }
221 | }
222 |
223 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
224 |
225 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
226 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
227 | exclude group:'com.squareup.okhttp3', module:'okhttp'
228 | }
229 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
230 |
231 | if (hermesEnabled.toBoolean()) {
232 | implementation("com.facebook.react:hermes-android")
233 | } else {
234 | implementation jscFlavor
235 | }
236 | }
237 |
238 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
239 | applyNativeModulesAppBuildGradle(project)
240 |
241 | apply plugin: 'com.google.gms.google-services'
242 | apply plugin: 'com.google.firebase.firebase-perf'
243 | apply plugin: 'com.google.firebase.crashlytics'
244 |
--------------------------------------------------------------------------------
/store/actions/events.tsx:
--------------------------------------------------------------------------------
1 | import analytics from "@react-native-firebase/analytics";
2 | import crashlytics from "@react-native-firebase/crashlytics";
3 | import { deleteSavedEvent, fetchSavedEvents, insertSavedEvent } from "../../helpers/sqlite_db";
4 | import { Alert } from "react-native";
5 | import { Event } from "../../interfaces/event";
6 |
7 | export const ADD_EVENT = "ADD_EVENT";
8 | export const DELETE_EVENT = "DELETE_EVENT";
9 | export const SAVE_EVENT = "SAVE_EVENT";
10 | export const SET_EVENTS = "SET_EVENTS";
11 | export const SET_SAVED_EVENTS = "SET_SAVED_EVENTS";
12 | export const UPDATE_EVENT = "UPDATE_EVENT";
13 |
14 | export const addEvent = (event: Event) => {
15 | return async (
16 | dispatch: (arg0: {
17 | type: string;
18 | eventData: {
19 | id: number | string;
20 | date: Date;
21 | description: string;
22 | firestoreDocumentId: string;
23 | imageUrl: string;
24 | isUserEvent: boolean;
25 | latitude: number;
26 | longitude: number;
27 | title: string;
28 | };
29 | }) => void
30 | ) => {
31 | try {
32 | dispatch({
33 | type: ADD_EVENT,
34 | eventData: {
35 | id: event.id,
36 | date: event.date,
37 | description: event.description,
38 | firestoreDocumentId: event.firestoreDocumentId!,
39 | imageUrl: event.imageUrl,
40 | isUserEvent: event.isUserEvent,
41 | latitude: event.latitude,
42 | longitude: event.longitude,
43 | title: event.title,
44 | },
45 | });
46 | analytics().logEvent("custom_log", {
47 | description: "--- Analytics: store -> actions -> events -> addEvent -> try, event: " + event,
48 | });
49 | } catch (error: unknown) {
50 | if (error instanceof Error) {
51 | Alert.alert(
52 | "Error ❌",
53 | "Adding an event has failed.",
54 | [{ text: "Okay" }]
55 | );
56 |
57 | analytics().logEvent("custom_log", {
58 | description: "--- Analytics: store -> actions -> events -> addEvent -> catch, error: " + error,
59 | });
60 | crashlytics().recordError(error);
61 | }
62 | } finally {
63 | Alert.alert(
64 | "Event added ✅",
65 | "You have successfully added this event.",
66 | [{ text: "Okay" }]
67 | );
68 | analytics().logEvent("custom_log", {
69 | description: "--- Analytics: store -> actions -> events -> addEvent -> finally",
70 | });
71 | }
72 | };
73 | };
74 |
75 | export const deleteEvent = (event: Event) => {
76 | return async (
77 | dispatch: (arg0: {
78 | type: string;
79 | eventData: {
80 | id: number | string;
81 | date: Date;
82 | description: string;
83 | firestoreDocumentId?: string;
84 | imageUrl: string;
85 | isUserEvent: boolean;
86 | latitude: number;
87 | longitude: number;
88 | title: string;
89 | };
90 | }) => void
91 | ) => {
92 | try {
93 | const dbResult: any = await deleteSavedEvent(event.id);
94 |
95 | dispatch({
96 | type: DELETE_EVENT,
97 | eventData: {
98 | id: event.id,
99 | date: event.date,
100 | description: event.description,
101 | firestoreDocumentId: event.firestoreDocumentId,
102 | imageUrl: event.imageUrl,
103 | isUserEvent: event.isUserEvent,
104 | latitude: event.latitude,
105 | longitude: event.longitude,
106 | title: event.title,
107 | },
108 | });
109 | analytics().logEvent("custom_log", {
110 | description: "--- Analytics: store -> actions -> events -> deleteEvent -> try, dbResult: " + dbResult,
111 | });
112 | analytics().logEvent("custom_log", {
113 | description: "--- Analytics: store -> actions -> events -> deleteEvent -> try, event: " + event,
114 | });
115 | } catch (error: unknown) {
116 | if (error instanceof Error) {
117 | Alert.alert(
118 | "Error ❌",
119 | "Deleting a saved event has failed.",
120 | [{ text: "Okay" }]
121 | );
122 |
123 | analytics().logEvent("custom_log", {
124 | description: "--- Analytics: store -> actions -> events -> deleteEvent -> catch, error: " + error,
125 | });
126 | crashlytics().recordError(error);
127 | }
128 | } finally {
129 | Alert.alert(
130 | "Saved event deleted ✅",
131 | "You have successfully deleted this saved event, and it will no longer be visible.",
132 | [{ text: "Okay" }]
133 | );
134 | analytics().logEvent("custom_log", {
135 | description: "--- Analytics: store -> actions -> events -> deleteEvent -> finally",
136 | });
137 | }
138 | };
139 | };
140 |
141 | export const fetchUsersSavedEvents = () => {
142 | return async (
143 | dispatch: (arg0: { savedEvents: Event[]; type: string }) => void
144 | ) => {
145 | try {
146 | const dbResult: any = await fetchSavedEvents();
147 |
148 | dispatch({
149 | savedEvents: dbResult.rows._array,
150 | type: SET_SAVED_EVENTS
151 | });
152 | analytics().logEvent("custom_log", {
153 | description: "--- Analytics: store -> actions -> events -> fetchUsersSavedEvents -> try, dbResult: " + dbResult,
154 | });
155 | } catch (error: unknown) {
156 | if (error instanceof Error) {
157 | Alert.alert(
158 | "Error ❌",
159 | "TamoTam couldn't fetch your saved events.",
160 | [{ text: "Okay" }]
161 | );
162 |
163 | analytics().logEvent("custom_log", {
164 | description: "--- Analytics: store -> actions -> events -> fetchUsersSavedEvents -> catch, error: " + error,
165 | });
166 | crashlytics().recordError(error);
167 | }
168 | } finally {
169 | Alert.alert(
170 | "Saved events loaded ✅",
171 | "These are stored on your local device as long as you won't explicitly clear the data or uninstall TamoTam.",
172 | [{ text: "Okay" }]
173 | );
174 | analytics().logEvent("custom_log", {
175 | description: "--- Analytics: store -> actions -> events -> fetchUsersSavedEvents -> finally",
176 | });
177 | }
178 | };
179 | };
180 |
181 | export const readItemFromStorage: (eventsFromAsyncStorage: Event[]) => void = (eventsFromAsyncStorage: Event[]) => {
182 | return async (
183 | dispatch: (arg0: { events: Event[]; type: string }) => void
184 | ) => {
185 | try {
186 | dispatch({
187 | type: SET_EVENTS,
188 | events: eventsFromAsyncStorage,
189 | });
190 | analytics().logEvent("custom_log", {
191 | description: "--- Analytics: store -> actions -> events -> readItemFromStorage -> try, eventsFromAsyncStorage: " + eventsFromAsyncStorage,
192 | });
193 | } catch (error: unknown) {
194 | if (error instanceof Error) {
195 | Alert.alert(
196 | "Error ❌",
197 | "Problem with loading events from a local device to avoid a big load during the next application launch.",
198 | [{ text: "Okay" }]
199 | );
200 |
201 | analytics().logEvent("custom_log", {
202 | description: "--- Analytics: store -> actions -> events -> readItemFromStorage -> catch, error: " + error,
203 | });
204 | crashlytics().recordError(error);
205 | }
206 | } finally {
207 | analytics().logEvent("custom_log", {
208 | description: "--- Analytics: store -> actions -> events -> readItemFromStorage -> finally",
209 | });
210 | }
211 | }
212 | };
213 |
214 | export const saveEvent = (event: Event) => {
215 | return async (
216 | dispatch: (arg0: {
217 | type: string;
218 | eventData: {
219 | id: number | string;
220 | date: Date;
221 | description: string;
222 | firestoreDocumentId?: string;
223 | imageUrl: string;
224 | isUserEvent: boolean;
225 | latitude: number;
226 | longitude: number;
227 | title: string;
228 | };
229 | }) => void
230 | ) => {
231 | try {
232 | const dbResult: any = await insertSavedEvent(
233 | event.date,
234 | event.description,
235 | event.firestoreDocumentId,
236 | event.imageUrl,
237 | event.isUserEvent,
238 | event.latitude,
239 | event.longitude,
240 | event.title
241 | );
242 |
243 | dispatch({
244 | type: SAVE_EVENT,
245 | eventData: {
246 | id: event.id,
247 | date: event.date,
248 | description: event.description,
249 | firestoreDocumentId: event.firestoreDocumentId,
250 | imageUrl: event.imageUrl,
251 | isUserEvent: event.isUserEvent,
252 | latitude: event.latitude,
253 | longitude: event.longitude,
254 | title: event.title,
255 | },
256 | });
257 | analytics().logEvent("custom_log", {
258 | description: "--- Analytics: store -> actions -> events -> saveEvent -> try, dbResult: " + dbResult,
259 | });
260 | analytics().logEvent("custom_log", {
261 | description: "--- Analytics: store -> actions -> events -> saveEvent -> try, event: " + event,
262 | });
263 | } catch (error: unknown) {
264 | if (error instanceof Error) {
265 | Alert.alert(
266 | "Error ❌",
267 | "TamoTam couldn't save your event.",
268 | [{ text: "Okay" }]
269 | );
270 |
271 | analytics().logEvent("custom_log", {
272 | description: "--- Analytics: store -> actions -> events -> saveEvent -> catch, error: " + error,
273 | });
274 | crashlytics().recordError(error);
275 | }
276 | } finally {
277 | Alert.alert(
278 | "Event saved ✅",
279 | "It will be visible in your saved events till you don't delete it.",
280 | [{ text: "Okay" }]
281 | );
282 | analytics().logEvent("custom_log", {
283 | description: "--- Analytics: store -> actions -> events -> saveEvent -> finally",
284 | });
285 | }
286 | };
287 | };
288 |
289 | export const updateEvent = (event: Event) => {
290 | return async (
291 | dispatch: (arg0: {
292 | type: string;
293 | eventData: {
294 | id: number | string;
295 | date: Date;
296 | description: string;
297 | firestoreDocumentId: string;
298 | imageUrl: string;
299 | isUserEvent: boolean;
300 | latitude: number;
301 | longitude: number;
302 | title: string;
303 | };
304 | }) => void
305 | ) => {
306 | try {
307 | dispatch({
308 | type: UPDATE_EVENT,
309 | eventData: {
310 | id: event.id,
311 | date: event.date,
312 | description: event.description,
313 | firestoreDocumentId: event.firestoreDocumentId!,
314 | imageUrl: event.imageUrl,
315 | isUserEvent: event.isUserEvent,
316 | latitude: event.latitude,
317 | longitude: event.longitude,
318 | title: event.title,
319 | },
320 | });
321 |
322 | analytics().logEvent("custom_log", {
323 | description: "--- Analytics: store -> actions -> events -> updateEvent -> try, event: " + event,
324 | });
325 | }
326 | catch (error: unknown) {
327 | if (error instanceof Error) {
328 | Alert.alert(
329 | "Error ❌",
330 | "TamoTam couldn't update this event.",
331 | [{ text: "Okay" }]
332 | );
333 |
334 | analytics().logEvent("custom_log", {
335 | description: "--- Analytics: store -> actions -> events -> updateEvent -> catch, error: " + error,
336 | });
337 | crashlytics().recordError(error);
338 | }
339 | } finally {
340 | Alert.alert(
341 | "Event updated ✅",
342 | "It will be visible in your saved events till you don't delete it.",
343 | [{ text: "Okay" }]
344 | );
345 | }
346 | };
347 | };
348 |
--------------------------------------------------------------------------------