Initial Commit
1
.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
16
android/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
|
||||
# Bundle artifacts
|
||||
*.jsbundle
|
||||
177
android/app/build.gradle
Normal file
@@ -0,0 +1,177 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
react {
|
||||
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
||||
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
|
||||
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||
|
||||
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
||||
// Use Expo CLI to bundle the app, this ensures the Metro config
|
||||
// works correctly with Expo projects.
|
||||
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
||||
bundleCommand = "export:embed"
|
||||
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace 'com.ormiapp.yeegolinkedinprototype'
|
||||
defaultConfig {
|
||||
applicationId 'com.ormiapp.yeegolinkedinprototype'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
|
||||
}
|
||||
}
|
||||
androidResources {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
|
||||
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||
// Accepts values in comma delimited lists, example:
|
||||
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
|
||||
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
|
||||
// Split option: 'foo,bar' -> ['foo', 'bar']
|
||||
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
|
||||
// Trim all elements in place.
|
||||
for (i in 0..<options.size()) options[i] = options[i].trim();
|
||||
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
|
||||
options -= ""
|
||||
|
||||
if (options.length > 0) {
|
||||
println "android.packagingOptions.$prop += $options ($options.length)"
|
||||
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
|
||||
options.each {
|
||||
android.packagingOptions[prop] += it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||
|
||||
if (isGifEnabled) {
|
||||
// For animated gif support
|
||||
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
|
||||
}
|
||||
|
||||
if (isWebpEnabled) {
|
||||
// For webp support
|
||||
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
|
||||
if (isWebpAnimatedEnabled) {
|
||||
// Animated webp support
|
||||
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
|
||||
}
|
||||
}
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
BIN
android/app/debug.keystore
Normal file
14
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# react-native-reanimated
|
||||
-keep class com.swmansion.reanimated.** { *; }
|
||||
-keep class com.facebook.react.turbomodule.** { *; }
|
||||
|
||||
# Add any project specific keep options here:
|
||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
31
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true">
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="yeegolinkedinprototype"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ormiapp.yeegolinkedinprototype
|
||||
import expo.modules.splashscreen.SplashScreenManager
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
// setTheme(R.style.AppTheme);
|
||||
// @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
|
||||
SplashScreenManager.registerOnActivity(this)
|
||||
// @generated end expo-splashscreen
|
||||
super.onCreate(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "main"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||
return ReactActivityDelegateWrapper(
|
||||
this,
|
||||
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
object : DefaultReactActivityDelegate(
|
||||
this,
|
||||
mainComponentName,
|
||||
fabricEnabled
|
||||
){})
|
||||
}
|
||||
|
||||
/**
|
||||
* Align the back button behavior with Android S
|
||||
* where moving root activities to background instead of finishing activities.
|
||||
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
|
||||
*/
|
||||
override fun invokeDefaultOnBackPressed() {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
if (!moveTaskToBack(false)) {
|
||||
// For non-root activities, use the default implementation to finish them.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Use the default back button implementation on Android S
|
||||
// because it's doing more than [Activity.moveTaskToBack] in fact.
|
||||
super.invokeDefaultOnBackPressed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ormiapp.yeegolinkedinprototype
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
import expo.modules.ReactNativeHostWrapper
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
|
||||
this,
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> {
|
||||
val packages = PackageList(this).packages
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(MyReactNativePackage())
|
||||
return packages
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
}
|
||||
)
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
}
|
||||
BIN
android/app/src/main/res/drawable-hdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,6 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/splashscreen_background"/>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
37
android/app/src/main/res/drawable/rn_edit_text_material.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
|
||||
>
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
|
||||
</inset>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 18 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 24 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
android/app/src/main/res/values-night/colors.xml
Normal file
@@ -0,0 +1 @@
|
||||
<resources/>
|
||||
6
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<color name="splashscreen_background">#ffffff</color>
|
||||
<color name="iconBackground">#ffffff</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#ffffff</color>
|
||||
</resources>
|
||||
6
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<string name="app_name">yeego-linkedin-prototype</string>
|
||||
<string name="expo_system_ui_user_interface_style" translatable="false">automatic</string>
|
||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
</resources>
|
||||
12
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="AppTheme" parent="Theme.EdgeToEdge">
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:statusBarColor">#ffffff</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
</resources>
|
||||
37
android/build.gradle
Normal file
@@ -0,0 +1,37 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||
}
|
||||
}
|
||||
|
||||
def reactNativeAndroidDir = new File(
|
||||
providers.exec {
|
||||
workingDir(rootDir)
|
||||
commandLine("node", "--print", "require.resolve('react-native/package.json')")
|
||||
}.standardOutput.asText.get().trim(),
|
||||
"../android"
|
||||
)
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url(reactNativeAndroidDir)
|
||||
}
|
||||
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "expo-root-project"
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
59
android/gradle.properties
Normal file
@@ -0,0 +1,59 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
|
||||
# Enable AAPT2 PNG crunching
|
||||
android.enablePngCrunchInReleaseBuilds=true
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
# ./gradlew <task> -PreactNativeArchitectures=x86_64
|
||||
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
||||
|
||||
# Use this property to enable support to the new architecture.
|
||||
# This will allow you to use TurboModules and the Fabric render in
|
||||
# your application. You should enable this flag either if you want
|
||||
# to write custom TurboModules/Fabric components OR use libraries that
|
||||
# are providing them.
|
||||
newArchEnabled=true
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
# Enable GIF support in React Native images (~200 B increase)
|
||||
expo.gif.enabled=true
|
||||
# Enable webp support in React Native images (~85 KB increase)
|
||||
expo.webp.enabled=true
|
||||
# Enable animated webp support (~3.4 MB increase)
|
||||
# Disabled by default because iOS doesn't support animated webp
|
||||
expo.webp.animated=false
|
||||
|
||||
# Enable network inspector
|
||||
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||
|
||||
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||
expo.useLegacyPackaging=false
|
||||
|
||||
# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
|
||||
expo.edgeToEdgeEnabled=true
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
7
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
251
android/gradlew
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
94
android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
39
android/settings.gradle
Normal file
@@ -0,0 +1,39 @@
|
||||
pluginManagement {
|
||||
def reactNativeGradlePlugin = new File(
|
||||
providers.exec {
|
||||
workingDir(rootDir)
|
||||
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
|
||||
}.standardOutput.asText.get().trim()
|
||||
).getParentFile().absolutePath
|
||||
includeBuild(reactNativeGradlePlugin)
|
||||
|
||||
def expoPluginsPath = new File(
|
||||
providers.exec {
|
||||
workingDir(rootDir)
|
||||
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
|
||||
}.standardOutput.asText.get().trim(),
|
||||
"../android/expo-gradle-plugin"
|
||||
).absolutePath
|
||||
includeBuild(expoPluginsPath)
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.facebook.react.settings")
|
||||
id("expo-autolinking-settings")
|
||||
}
|
||||
|
||||
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
||||
ex.autolinkLibrariesFromCommand()
|
||||
} else {
|
||||
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
|
||||
}
|
||||
}
|
||||
expoAutolinking.useExpoModules()
|
||||
|
||||
rootProject.name = 'yeego-linkedin-prototype'
|
||||
|
||||
expoAutolinking.useExpoVersionCatalog()
|
||||
|
||||
include ':app'
|
||||
includeBuild(expoAutolinking.reactNativeGradlePlugin)
|
||||
279
api/LinkedInAPI.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import { UNIPILE_CONFIG, unipileFetch } from '@/utils/unipile'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
|
||||
export interface CreateHostedAuthLinkRequest {
|
||||
type: "create" | "reconnect"
|
||||
api_url?: string
|
||||
expiresOn: string
|
||||
providers: string | string[]
|
||||
notify_url?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface CreateHostedAuthLinkResponse {
|
||||
url: string
|
||||
id: string
|
||||
expiresOn: string
|
||||
}
|
||||
|
||||
export interface LinkedInProfile {
|
||||
object: string
|
||||
provider: string
|
||||
provider_id: string
|
||||
public_identifier: string
|
||||
member_urn: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
headline?: string
|
||||
primary_locale?: {
|
||||
country: string
|
||||
language: string
|
||||
}
|
||||
is_open_profile: boolean
|
||||
is_premium: boolean
|
||||
is_influencer: boolean
|
||||
is_creator: boolean
|
||||
is_relationship: boolean
|
||||
network_distance?: string
|
||||
is_self: boolean
|
||||
websites?: any[]
|
||||
follower_count?: number
|
||||
connections_count?: number
|
||||
location?: string
|
||||
birthdate?: {
|
||||
month: number
|
||||
day: number
|
||||
}
|
||||
invitation?: {
|
||||
type: string
|
||||
status: string
|
||||
}
|
||||
profile_picture_url?: string
|
||||
profile_picture_url_large?: string
|
||||
// Legacy fields
|
||||
id?: string
|
||||
identifier?: string
|
||||
display_name?: string
|
||||
summary?: string
|
||||
industry?: string
|
||||
public_profile_url?: string
|
||||
connection_count?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export interface SendInvitationRequest {
|
||||
provider_id: string
|
||||
account_id: string
|
||||
}
|
||||
|
||||
export interface SendInvitationResponse {
|
||||
object: string
|
||||
invitation_id: string
|
||||
usage: number
|
||||
}
|
||||
|
||||
export interface CancelInvitationRequest {
|
||||
invitationId: string
|
||||
accountId: string
|
||||
}
|
||||
|
||||
export interface Invitation {
|
||||
id: string
|
||||
object: string
|
||||
date: string
|
||||
parsed_datetime: string
|
||||
invitation_text?: string
|
||||
invited_user: string
|
||||
invited_user_description?: string
|
||||
invited_user_id: string
|
||||
invited_user_public_id: string
|
||||
}
|
||||
|
||||
export interface InvitationsListResponse {
|
||||
items: Invitation[]
|
||||
has_more: boolean
|
||||
total_count?: number
|
||||
cursor?: string
|
||||
}
|
||||
|
||||
export interface GetInvitationsParams {
|
||||
accountId: string
|
||||
cursor?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export const LinkedInAPI = {
|
||||
// Create hosted authentication link
|
||||
async createHostedAuthLink(request: CreateHostedAuthLinkRequest): Promise<CreateHostedAuthLinkResponse> {
|
||||
try {
|
||||
const response = await unipileFetch(`${UNIPILE_CONFIG.BASE_URL}/api/v1/hosted/accounts/link`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(
|
||||
`Failed to create hosted auth link: ${typeof errorData === 'string' ? errorData : JSON.stringify(errorData) || response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to create hosted auth link: ${error?.message || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Retrieve LinkedIn profile by public identifier
|
||||
async getProfileByIdentifier(identifier: string, accountId: string): Promise<LinkedInProfile> {
|
||||
try {
|
||||
const response = await unipileFetch(`${UNIPILE_CONFIG.BASE_URL}/api/v1/users/${identifier}?account_id=${accountId}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(
|
||||
`Failed to retrieve profile: ${typeof errorData === 'string' ? errorData : JSON.stringify(errorData) || response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to retrieve profile: ${error?.message || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Send LinkedIn invitation
|
||||
async sendInvitation(request: SendInvitationRequest): Promise<SendInvitationResponse> {
|
||||
try {
|
||||
const response = await unipileFetch(`${UNIPILE_CONFIG.BASE_URL}/api/v1/users/invite`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(
|
||||
`Failed to send invitation: ${typeof errorData === 'string' ? errorData : JSON.stringify(errorData) || response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to send invitation: ${error?.message || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Fetch all invitations by account ID
|
||||
async getInvitations(accountId: string): Promise<InvitationsListResponse> {
|
||||
try {
|
||||
const response = await unipileFetch(`${UNIPILE_CONFIG.BASE_URL}/api/v1/users/invite/sent?account_id=${accountId}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(
|
||||
`Failed to fetch invitations: ${typeof errorData === 'string' ? errorData : JSON.stringify(errorData) || response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to fetch invitations: ${error?.message || 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch invitations with pagination support
|
||||
async getInvitationsPaginated({ accountId, cursor, limit = 100 }: GetInvitationsParams): Promise<InvitationsListResponse> {
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
account_id: accountId,
|
||||
limit: limit.toString()
|
||||
})
|
||||
|
||||
if (cursor) {
|
||||
queryParams.append('cursor', cursor)
|
||||
}
|
||||
|
||||
const url = `${UNIPILE_CONFIG.BASE_URL}/api/v1/users/invite/sent?${queryParams.toString()}`
|
||||
|
||||
const response = await unipileFetch(url, {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(
|
||||
`Failed to fetch invitations: ${typeof errorData === 'string' ? errorData : JSON.stringify(errorData) || response.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to fetch invitations: ${error?.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hook for creating hosted authentication link
|
||||
export const useCreateHostedAuthLink = () => {
|
||||
return useMutation({
|
||||
mutationFn: (request: CreateHostedAuthLinkRequest) => LinkedInAPI.createHostedAuthLink(request),
|
||||
})
|
||||
}
|
||||
|
||||
// Hook for retrieving LinkedIn profile by identifier
|
||||
export const useGetProfileByIdentifier = (identifier: string, accountId: string, enabled: boolean = true) => {
|
||||
return useQuery({
|
||||
queryKey: ['linkedin-profile', identifier],
|
||||
queryFn: () => LinkedInAPI.getProfileByIdentifier(identifier, accountId),
|
||||
enabled: enabled && !!identifier,
|
||||
})
|
||||
}
|
||||
|
||||
// Hook for sending LinkedIn invitation
|
||||
export const useSendInvitation = () => {
|
||||
return useMutation({
|
||||
mutationFn: (request: SendInvitationRequest) => LinkedInAPI.sendInvitation(request),
|
||||
})
|
||||
}
|
||||
|
||||
// Hook for fetching all invitations by account ID
|
||||
export const useGetInvitations = (accountId: string, enabled: boolean = true) => {
|
||||
return useQuery({
|
||||
queryKey: ['linkedin-invitations', accountId],
|
||||
queryFn: () => LinkedInAPI.getInvitations(accountId),
|
||||
enabled: enabled && !!accountId,
|
||||
})
|
||||
}
|
||||
|
||||
// Hook for getting LinkedIn profile and connection status
|
||||
export const useLinkedInProfileAndStatus = (
|
||||
linkedInProfileId: string,
|
||||
accountId: string,
|
||||
enabled: boolean = true
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ['linkedin-profile-status', linkedInProfileId, accountId],
|
||||
queryFn: async () => {
|
||||
const profile = await LinkedInAPI.getProfileByIdentifier(linkedInProfileId, accountId)
|
||||
const { getConnectionStatusFromProfile } = await import('@/helper/linkedinConnect')
|
||||
const connectionStatus = getConnectionStatusFromProfile(profile)
|
||||
|
||||
return {
|
||||
profile,
|
||||
connectionStatus
|
||||
}
|
||||
},
|
||||
enabled: enabled && !!linkedInProfileId && !!accountId,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true,
|
||||
staleTime: 0,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
})
|
||||
}
|
||||
91
api/ProfileAPI.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { CreateProfileData, Profile } from '@/types/profile'
|
||||
import { supabase } from '@/utils/supabase'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
|
||||
export const ProfileAPI = {
|
||||
// Get all profiles
|
||||
async getProfiles(): Promise<Profile[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.order('updated_at', { ascending: false })
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch profiles: ${error.message}`)
|
||||
}
|
||||
|
||||
return data || []
|
||||
},
|
||||
|
||||
// Get profile by ID
|
||||
async getProfileById(id: string): Promise<Profile | null> {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
if (error.code === 'PGRST116') {
|
||||
return null
|
||||
}
|
||||
throw new Error(`Failed to fetch profile: ${error.message}`)
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
// Get current user's profile
|
||||
async getCurrentUserProfile(): Promise<Profile | null> {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.getProfileById(user.id)
|
||||
},
|
||||
|
||||
// Create new profile
|
||||
async createProfile(profileData: CreateProfileData): Promise<Profile> {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.insert([profileData])
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to create profile: ${error.message}`)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
export const useProfiles = () => {
|
||||
return useQuery({
|
||||
queryKey: ['profiles'],
|
||||
queryFn: ProfileAPI.getProfiles,
|
||||
})
|
||||
}
|
||||
|
||||
export const useProfile = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['profile', id],
|
||||
queryFn: () => ProfileAPI.getProfileById(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCreateProfile = () => {
|
||||
return useMutation({
|
||||
mutationFn: ProfileAPI.createProfile,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCurrentUserProfile = () => {
|
||||
return useQuery({
|
||||
queryKey: ['currentUserProfile'],
|
||||
queryFn: ProfileAPI.getCurrentUserProfile,
|
||||
})
|
||||
}
|
||||
17
app.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "yeego-linkedin-prototype",
|
||||
"slug": "yeego-linkedin-prototype",
|
||||
"slug": "yeego-prototype",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
@@ -16,7 +16,8 @@
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"edgeToEdgeEnabled": true
|
||||
"edgeToEdgeEnabled": true,
|
||||
"package": "com.ormiapp.yeegolinkedinprototype"
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
@@ -33,10 +34,18 @@
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
]
|
||||
],
|
||||
"expo-web-browser"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"router": {},
|
||||
"eas": {
|
||||
"projectId": "95f61149-9d88-4fd1-8d21-a30170a13909"
|
||||
}
|
||||
},
|
||||
"owner": "rappleit"
|
||||
}
|
||||
}
|
||||
|
||||
16
app/(auth)/_layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useContext, useEffect } from "react";
|
||||
|
||||
export default function AuthLayout() {
|
||||
const router = useRouter();
|
||||
const authContext = useContext(AuthContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (authContext?.user) {
|
||||
router.replace("/(tabs)");
|
||||
}
|
||||
}, [authContext?.user, router]);
|
||||
|
||||
return <Stack screenOptions={{ headerShown: false, animation: "none" }} />;
|
||||
}
|
||||
208
app/(auth)/login.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import { useRouter } from "expo-router";
|
||||
import React, { useContext, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
import { PageContainer } from "@/components/PageContainer";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ThemedButton } from "@/components/ui/ThemedButton";
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
import { useThemeColor } from "@/hooks/useThemeColor";
|
||||
|
||||
export default function LoginScreen() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const authContext = useContext(AuthContext);
|
||||
if (!authContext) {
|
||||
throw new Error("LoginScreen must be used within an AuthProvider");
|
||||
}
|
||||
const { login } = authContext;
|
||||
|
||||
const backgroundColor = useThemeColor({}, "background");
|
||||
const textColor = useThemeColor({}, "text");
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert("Error", "Please fill in all fields");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
"Login Failed",
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "An error occurred during login"
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPassword = () => {
|
||||
Alert.alert(
|
||||
"Forgot Password",
|
||||
"Password reset functionality will be implemented here"
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
style={[styles.container, { backgroundColor }]}
|
||||
scrollable={false}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.keyboardAvoidingView}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<ThemedText type="title" style={styles.title}>
|
||||
Welcome!
|
||||
</ThemedText>
|
||||
<ThemedText type="subtitle" style={styles.subtitle}>
|
||||
Log in to your account
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText type="defaultSemiBold" style={styles.label}>
|
||||
Email
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: useThemeColor({}, "cardBackground"),
|
||||
color: textColor,
|
||||
borderColor: useThemeColor({}, "icon"),
|
||||
},
|
||||
]}
|
||||
placeholder="Enter your email"
|
||||
placeholderTextColor={useThemeColor({}, "icon")}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText type="defaultSemiBold" style={styles.label}>
|
||||
Password
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: useThemeColor({}, "cardBackground"),
|
||||
color: textColor,
|
||||
},
|
||||
]}
|
||||
placeholder="Enter your password"
|
||||
placeholderTextColor={useThemeColor({}, "icon")}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ThemedButton
|
||||
title="Sign In"
|
||||
onPress={handleLogin}
|
||||
loading={isLoading}
|
||||
style={styles.loginButton}
|
||||
size="large"
|
||||
/>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<ThemedText type="default" style={styles.footerText}>
|
||||
Don't have an account?{" "}
|
||||
</ThemedText>
|
||||
<ThemedText
|
||||
type="link"
|
||||
style={styles.signUpLink}
|
||||
onPress={() => router.push("/(auth)/signup")}
|
||||
>
|
||||
Sign Up
|
||||
</ThemedText>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 40,
|
||||
},
|
||||
header: {
|
||||
alignItems: "center",
|
||||
marginBottom: 40,
|
||||
},
|
||||
title: {
|
||||
marginBottom: 8,
|
||||
textAlign: "center",
|
||||
},
|
||||
subtitle: {
|
||||
textAlign: "center",
|
||||
opacity: 0.7,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
height: 50,
|
||||
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
fontSize: 16,
|
||||
},
|
||||
loginButton: {
|
||||
marginTop: 10,
|
||||
marginBottom: 16,
|
||||
},
|
||||
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
footerText: {
|
||||
textAlign: "center",
|
||||
},
|
||||
signUpLink: {
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
});
|
||||
289
app/(auth)/signup.tsx
Normal file
@@ -0,0 +1,289 @@
|
||||
import { useRouter } from "expo-router";
|
||||
import React, { useContext, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
import { PageContainer } from "@/components/PageContainer";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ThemedButton } from "@/components/ui/ThemedButton";
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
import { useThemeColor } from "@/hooks/useThemeColor";
|
||||
|
||||
export default function SignupScreen() {
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const authContext = useContext(AuthContext);
|
||||
if (!authContext) {
|
||||
throw new Error("SignupScreen must be used within an AuthProvider");
|
||||
}
|
||||
const { register } = authContext;
|
||||
|
||||
const backgroundColor = useThemeColor({}, "background");
|
||||
const textColor = useThemeColor({}, "text");
|
||||
|
||||
const handleSignup = async () => {
|
||||
if (!displayName || !username || !email || !password || !confirmPassword) {
|
||||
Alert.alert("Error", "Please fill in all fields");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
Alert.alert("Error", "Passwords do not match");
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
Alert.alert("Error", "Password must be at least 6 characters long");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await register(email, password, {
|
||||
username: username,
|
||||
display_name: displayName,
|
||||
});
|
||||
Alert.alert("Success", "Account created successfully!", [
|
||||
{
|
||||
text: "OK",
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
"Registration Failed",
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "An error occurred during registration"
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
style={[styles.container, { backgroundColor }]}
|
||||
scrollable={false}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.keyboardAvoidingView}
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<ThemedText type="title" style={styles.title}>
|
||||
Create Account
|
||||
</ThemedText>
|
||||
<ThemedText type="subtitle" style={styles.subtitle}>
|
||||
Join us today
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText type="defaultSemiBold" style={styles.label}>
|
||||
Display Name
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: useThemeColor({}, "cardBackground"),
|
||||
color: textColor,
|
||||
borderColor: useThemeColor({}, "icon"),
|
||||
},
|
||||
]}
|
||||
placeholder="Enter your display name"
|
||||
placeholderTextColor={useThemeColor({}, "icon")}
|
||||
value={displayName}
|
||||
onChangeText={setDisplayName}
|
||||
autoCapitalize="words"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText type="defaultSemiBold" style={styles.label}>
|
||||
Username
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: useThemeColor({}, "cardBackground"),
|
||||
color: textColor,
|
||||
borderColor: useThemeColor({}, "icon"),
|
||||
},
|
||||
]}
|
||||
placeholder="Choose a username"
|
||||
placeholderTextColor={useThemeColor({}, "icon")}
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText type="defaultSemiBold" style={styles.label}>
|
||||
Email
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: useThemeColor({}, "cardBackground"),
|
||||
color: textColor,
|
||||
borderColor: useThemeColor({}, "icon"),
|
||||
},
|
||||
]}
|
||||
placeholder="Enter your email"
|
||||
placeholderTextColor={useThemeColor({}, "icon")}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText type="defaultSemiBold" style={styles.label}>
|
||||
Password
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: useThemeColor({}, "cardBackground"),
|
||||
color: textColor,
|
||||
borderColor: useThemeColor({}, "icon"),
|
||||
},
|
||||
]}
|
||||
placeholder="Create a password"
|
||||
placeholderTextColor={useThemeColor({}, "icon")}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText type="defaultSemiBold" style={styles.label}>
|
||||
Confirm Password
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
{
|
||||
backgroundColor: useThemeColor({}, "cardBackground"),
|
||||
color: textColor,
|
||||
borderColor: useThemeColor({}, "icon"),
|
||||
},
|
||||
]}
|
||||
placeholder="Confirm your password"
|
||||
placeholderTextColor={useThemeColor({}, "icon")}
|
||||
value={confirmPassword}
|
||||
onChangeText={setConfirmPassword}
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ThemedButton
|
||||
title="Create Account"
|
||||
onPress={handleSignup}
|
||||
loading={isLoading}
|
||||
style={styles.signupButton}
|
||||
size="large"
|
||||
/>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<ThemedText type="default" style={styles.footerText}>
|
||||
Already have an account?
|
||||
{" "}
|
||||
</ThemedText>
|
||||
<ThemedText
|
||||
type="link"
|
||||
style={styles.signInLink}
|
||||
onPress={() => router.push("/(auth)/login")}
|
||||
>
|
||||
Log In
|
||||
</ThemedText>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 40,
|
||||
},
|
||||
header: {
|
||||
alignItems: "center",
|
||||
marginBottom: 30,
|
||||
},
|
||||
title: {
|
||||
marginBottom: 8,
|
||||
textAlign: "center",
|
||||
},
|
||||
subtitle: {
|
||||
textAlign: "center",
|
||||
opacity: 0.7,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
height: 50,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
fontSize: 16,
|
||||
},
|
||||
signupButton: {
|
||||
marginTop: 10,
|
||||
marginBottom: 20,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
footerText: {
|
||||
textAlign: "center",
|
||||
},
|
||||
signInLink: {
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { AuthContext } from '@/contexts/AuthContext';
|
||||
import { Tabs, useRouter } from 'expo-router';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import { HapticTab } from '@/components/HapticTab';
|
||||
@@ -9,8 +10,20 @@ import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
export default function TabLayout() {
|
||||
const router = useRouter();
|
||||
const authContext = useContext(AuthContext);
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authContext?.user) {
|
||||
router.replace('/(auth)/login');
|
||||
}
|
||||
}, [authContext?.user, router]);
|
||||
|
||||
if (!authContext?.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
@@ -33,11 +46,25 @@ export default function TabLayout() {
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tabs.Screen
|
||||
name="explore"
|
||||
name="profiles/index"
|
||||
options={{
|
||||
title: 'Explore',
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
|
||||
title: 'Discover',
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="magnifyingglass" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="profiles/[id]"
|
||||
options={{
|
||||
href: null,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
title: 'Settings',
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="gear" color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { Image } from 'expo-image';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
|
||||
import { Collapsible } from '@/components/Collapsible';
|
||||
import { ExternalLink } from '@/components/ExternalLink';
|
||||
import ParallaxScrollView from '@/components/ParallaxScrollView';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
|
||||
export default function TabTwoScreen() {
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
|
||||
headerImage={
|
||||
<IconSymbol
|
||||
size={310}
|
||||
color="#808080"
|
||||
name="chevron.left.forwardslash.chevron.right"
|
||||
style={styles.headerImage}
|
||||
/>
|
||||
}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText type="title">Explore</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedText>This app includes example code to help you get started.</ThemedText>
|
||||
<Collapsible title="File-based routing">
|
||||
<ThemedText>
|
||||
This app has two screens:{' '}
|
||||
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
|
||||
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
|
||||
</ThemedText>
|
||||
<ThemedText>
|
||||
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
|
||||
sets up the tab navigator.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Android, iOS, and web support">
|
||||
<ThemedText>
|
||||
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
|
||||
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
|
||||
</ThemedText>
|
||||
</Collapsible>
|
||||
<Collapsible title="Images">
|
||||
<ThemedText>
|
||||
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
|
||||
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
|
||||
different screen densities
|
||||
</ThemedText>
|
||||
<Image source={require('@/assets/images/react-logo.png')} style={{ alignSelf: 'center' }} />
|
||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Custom fonts">
|
||||
<ThemedText>
|
||||
Open <ThemedText type="defaultSemiBold">app/_layout.tsx</ThemedText> to see how to load{' '}
|
||||
<ThemedText style={{ fontFamily: 'SpaceMono' }}>
|
||||
custom fonts such as this one.
|
||||
</ThemedText>
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/versions/latest/sdk/font">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Light and dark mode components">
|
||||
<ThemedText>
|
||||
This template has light and dark mode support. The{' '}
|
||||
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
|
||||
what the user's current color scheme is, and so you can adjust UI colors accordingly.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Animations">
|
||||
<ThemedText>
|
||||
This template includes an example of an animated component. The{' '}
|
||||
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
|
||||
the powerful <ThemedText type="defaultSemiBold">react-native-reanimated</ThemedText>{' '}
|
||||
library to create a waving hand animation.
|
||||
</ThemedText>
|
||||
{Platform.select({
|
||||
ios: (
|
||||
<ThemedText>
|
||||
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
|
||||
component provides a parallax effect for the header image.
|
||||
</ThemedText>
|
||||
),
|
||||
})}
|
||||
</Collapsible>
|
||||
</ParallaxScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerImage: {
|
||||
color: '#808080',
|
||||
bottom: -90,
|
||||
left: -35,
|
||||
position: 'absolute',
|
||||
},
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
});
|
||||
@@ -1,75 +1,14 @@
|
||||
import { Image } from 'expo-image';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
|
||||
import { HelloWave } from '@/components/HelloWave';
|
||||
import ParallaxScrollView from '@/components/ParallaxScrollView';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import ProfileScreen from "@/components/profile/ProfileScreen";
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function HomeScreen() {
|
||||
const authContext = useContext(AuthContext);
|
||||
const userId = authContext?.user?.id || "";
|
||||
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
|
||||
headerImage={
|
||||
<Image
|
||||
source={require('@/assets/images/partial-react-logo.png')}
|
||||
style={styles.reactLogo}
|
||||
/>
|
||||
}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText type="title">Welcome!</ThemedText>
|
||||
<HelloWave />
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
|
||||
<ThemedText>
|
||||
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
|
||||
Press{' '}
|
||||
<ThemedText type="defaultSemiBold">
|
||||
{Platform.select({
|
||||
ios: 'cmd + d',
|
||||
android: 'cmd + m',
|
||||
web: 'F12',
|
||||
})}
|
||||
</ThemedText>{' '}
|
||||
to open developer tools.
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
|
||||
<ThemedText>
|
||||
{`Tap the Explore tab to learn more about what's included in this starter app.`}
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.stepContainer}>
|
||||
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
|
||||
<ThemedText>
|
||||
{`When you're ready, run `}
|
||||
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
|
||||
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
|
||||
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
|
||||
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
</ParallaxScrollView>
|
||||
<ProfileScreen userId={userId} />
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
stepContainer: {
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
reactLogo: {
|
||||
height: 178,
|
||||
width: 290,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
10
app/(tabs)/profiles/[id].tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useLocalSearchParams } from 'expo-router';
|
||||
import React from 'react';
|
||||
|
||||
import ProfileScreen from '@/components/profile/ProfileScreen';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
|
||||
return <ProfileScreen userId={id || ''} />;
|
||||
}
|
||||
148
app/(tabs)/profiles/index.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Link } from 'expo-router';
|
||||
import React, { useContext } from 'react';
|
||||
import { FlatList, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
import { useProfiles } from '@/api/ProfileAPI';
|
||||
import { PageContainer } from '@/components/PageContainer';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { ProfilePicture } from '@/components/profile/ProfilePicture';
|
||||
import { ThemedCard } from '@/components/ui/ThemedCard';
|
||||
import { AuthContext } from '@/contexts/AuthContext';
|
||||
import { Profile } from '@/types/profile';
|
||||
|
||||
type ProfileItemProps = {
|
||||
profile: Profile;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
function ProfileItem({ profile, onPress }: ProfileItemProps) {
|
||||
return (
|
||||
<Link href={`./profiles/${profile.id}`} asChild>
|
||||
<TouchableOpacity activeOpacity={0.7}>
|
||||
<ThemedCard style={styles.profileCard} backgroundColor='cardBackground'>
|
||||
<View style={styles.profileContent}>
|
||||
<ProfilePicture
|
||||
source={`https://api.dicebear.com/9.x/thumbs/png?seed=${profile.id}`}
|
||||
size={60}
|
||||
style={styles.profileImage}
|
||||
/>
|
||||
<View style={styles.profileInfo}>
|
||||
<ThemedText type="title" style={styles.profileName}>
|
||||
{profile.display_name || "Unknown User"}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.profileTitle}>
|
||||
@{profile.username || "no-username"}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
</ThemedCard>
|
||||
</TouchableOpacity>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProfilesIndex() {
|
||||
const authContext = useContext(AuthContext);
|
||||
const currentUserId = authContext?.user?.id;
|
||||
const { data: allProfiles, isLoading, error } = useProfiles();
|
||||
|
||||
const filteredProfiles = allProfiles?.filter(profile => profile.id !== currentUserId) || [];
|
||||
|
||||
const renderProfileItem = ({ item }: { item: Profile }) => (
|
||||
<ProfileItem
|
||||
profile={item}
|
||||
onPress={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageContainer style={styles.container} scrollable={false}>
|
||||
<ThemedText>Loading profiles...</ThemedText>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<PageContainer style={styles.container} scrollable={false}>
|
||||
<ThemedText>Error loading profiles: {error.message}</ThemedText>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer style={styles.container} scrollable={false}>
|
||||
<ThemedView style={styles.header}>
|
||||
<ThemedText type="title" style={styles.headerTitle}>
|
||||
Profiles
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.headerSubtitle}>
|
||||
Discover and connect with professionals
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
|
||||
<FlatList
|
||||
data={filteredProfiles}
|
||||
renderItem={renderProfileItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ItemSeparatorComponent={() => <ThemedView style={styles.separator} />}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
paddingTop: 20,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
headerSubtitle: {
|
||||
fontSize: 16,
|
||||
opacity: 0.7,
|
||||
},
|
||||
listContainer: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
profileCard: {
|
||||
marginVertical: 4,
|
||||
},
|
||||
profileContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
profileImage: {
|
||||
marginRight: 16,
|
||||
},
|
||||
profileInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
profileName: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
marginBottom: 2,
|
||||
},
|
||||
profileTitle: {
|
||||
fontSize: 14,
|
||||
opacity: 0.8,
|
||||
marginBottom: 2,
|
||||
},
|
||||
profileCompany: {
|
||||
fontSize: 14,
|
||||
opacity: 0.6,
|
||||
},
|
||||
separator: {
|
||||
height: 8,
|
||||
},
|
||||
});
|
||||
198
app/(tabs)/settings.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { LinkedInAPI } from "@/api/LinkedInAPI";
|
||||
import { PageContainer } from "@/components/PageContainer";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
import { IconSymbol } from "@/components/ui/IconSymbol";
|
||||
import { ThemedCard } from "@/components/ui/ThemedCard";
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
import Entypo from "@expo/vector-icons/Entypo";
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import { useContext, useEffect } from "react";
|
||||
import { Alert, Pressable, StyleSheet, View } from "react-native";
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const authContext = useContext(AuthContext);
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams();
|
||||
|
||||
if (!authContext) {
|
||||
throw new Error("SettingsScreen must be used within an AuthProvider");
|
||||
}
|
||||
const { logout, profile, profileLoading, refreshProfile } = authContext;
|
||||
|
||||
// Handle return from hosted auth
|
||||
useEffect(() => {
|
||||
// Check if user returned from auth flow
|
||||
if (params.auth === 'success') {
|
||||
Alert.alert("Success", "LinkedIn account connected successfully!");
|
||||
// Refresh profile to get updated LinkedIn connection status
|
||||
refreshProfile();
|
||||
|
||||
router.setParams({});
|
||||
} else if (params.auth === 'failure') {
|
||||
Alert.alert("Failed", "LinkedIn connection failed. Please try again.");
|
||||
router.setParams({});
|
||||
}
|
||||
}, [params.auth, router, refreshProfile]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
Alert.alert("Success", "You have been logged out successfully");
|
||||
} catch (error) {
|
||||
Alert.alert("Logout Failed", "An error occurred during logout");
|
||||
console.error("Logout failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnectLinkedIn = async () => {
|
||||
try {
|
||||
const request = {
|
||||
type: "create" as const,
|
||||
api_url: "https://api14.unipile.com:14496",
|
||||
expiresOn: new Date(Date.now() + 10 * 60 * 1000).toISOString(), //10 minutes
|
||||
providers: ["LINKEDIN"],
|
||||
notify_url: "https://rzzhsadiatfvgyiqdjmd.supabase.co/functions/v1/unipile-webhook",
|
||||
name: authContext.user?.id || "unknown_user",
|
||||
};
|
||||
|
||||
const { url } = await LinkedInAPI.createHostedAuthLink(request);
|
||||
console.log("Received hosted auth URL:", url);
|
||||
|
||||
Alert.alert(
|
||||
"Redirect to LinkedIn",
|
||||
"You will be redirected to LinkedIn to log in. Continue?",
|
||||
[
|
||||
{ text: "Cancel", style: "cancel" },
|
||||
{
|
||||
text: "Continue",
|
||||
onPress: async () => {
|
||||
try {
|
||||
const result = await WebBrowser.openAuthSessionAsync(url, 'your-app-scheme://');
|
||||
console.log("WebBrowser result:", result);
|
||||
|
||||
setTimeout(() => {
|
||||
refreshProfile();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error("WebBrowser error:", error);
|
||||
Alert.alert("Error", "Failed to open LinkedIn authentication.");
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
} catch (error) {
|
||||
Alert.alert("Error", "Failed to create LinkedIn auth link.");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderLinkedInCard = () => {
|
||||
if (profileLoading) {
|
||||
return (
|
||||
<ThemedCard backgroundColor="cardBackground" style={styles.iconCard}>
|
||||
<Entypo name="linkedin" size={24} color="#687076" />
|
||||
<ThemedText type="defaultSemiBold" style={styles.loadingText}>
|
||||
Loading LinkedIn status...
|
||||
</ThemedText>
|
||||
</ThemedCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (profile?.linkedin_connected) {
|
||||
return (
|
||||
<ThemedCard
|
||||
backgroundColor="tint"
|
||||
lightColor="#4CAF50"
|
||||
darkColor="#66BB6A"
|
||||
style={styles.iconCard}
|
||||
>
|
||||
<Entypo name="linkedin" size={24} color="#fff" />
|
||||
<View style={styles.linkedInInfo}>
|
||||
<ThemedText type="defaultSemiBold" style={styles.linkedInText}>
|
||||
LinkedIn Connected
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.linkedInProfileId}>
|
||||
Profile ID: {profile.linkedin_profile_id || 'N/A'}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</ThemedCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable onPress={handleConnectLinkedIn}>
|
||||
<ThemedCard backgroundColor="tint" style={styles.iconCard}>
|
||||
<Entypo name="linkedin" size={24} color="#fff" />
|
||||
<ThemedText type="defaultSemiBold" style={styles.linkedInText}>
|
||||
Connect account to LinkedIn
|
||||
</ThemedText>
|
||||
</ThemedCard>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText type="title">Settings</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedView style={styles.contentContainer}>
|
||||
{renderLinkedInCard()}
|
||||
<Pressable onPress={handleLogout}>
|
||||
<ThemedCard backgroundColor="cardBackground" style={styles.iconCard}>
|
||||
<IconSymbol
|
||||
name="rectangle.portrait.and.arrow.right"
|
||||
size={24}
|
||||
color="#000"
|
||||
/>
|
||||
<ThemedText type="defaultSemiBold" >
|
||||
Logout
|
||||
</ThemedText>
|
||||
</ThemedCard>
|
||||
</Pressable>
|
||||
</ThemedView>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 24,
|
||||
},
|
||||
contentContainer: {
|
||||
gap: 8,
|
||||
marginBottom: 24,
|
||||
},
|
||||
reactLogo: {
|
||||
height: 178,
|
||||
width: 290,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: "absolute",
|
||||
},
|
||||
linkedInText: {
|
||||
color: "#fff",
|
||||
},
|
||||
linkedInInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
linkedInProfileId: {
|
||||
color: "#fff",
|
||||
fontSize: 12,
|
||||
opacity: 0.8,
|
||||
},
|
||||
iconCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
},
|
||||
loadingText: {
|
||||
color: "#687076",
|
||||
},
|
||||
});
|
||||
@@ -1,29 +1,28 @@
|
||||
import { Link, Stack } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { PageContainer } from '@/components/PageContainer';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: 'Oops!' }} />
|
||||
<ThemedView style={styles.container}>
|
||||
<PageContainer scrollable={false} style={styles.container}>
|
||||
<ThemedText type="title">This screen does not exist.</ThemedText>
|
||||
<Link href="/" style={styles.link}>
|
||||
<ThemedText type="link">Go to home screen!</ThemedText>
|
||||
</Link>
|
||||
</ThemedView>
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
|
||||
@@ -2,8 +2,12 @@ import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native
|
||||
import { useFonts } from 'expo-font';
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { View } from 'react-native';
|
||||
import 'react-native-reanimated';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
|
||||
import { QueryProvider } from '@/components/QueryProvider';
|
||||
import { AuthProvider } from '@/contexts/AuthContext';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
export default function RootLayout() {
|
||||
@@ -14,16 +18,25 @@ export default function RootLayout() {
|
||||
|
||||
if (!loaded) {
|
||||
// Async font loading only occurs in development.
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
<QueryProvider>
|
||||
<AuthProvider>
|
||||
<SafeAreaProvider>
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<View style={{ flex: 1}}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
</View>
|
||||
<StatusBar style="auto" />
|
||||
</ThemeProvider>
|
||||
</SafeAreaProvider>
|
||||
</AuthProvider>
|
||||
</QueryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { StyleSheet, TouchableOpacity } from 'react-native';
|
||||
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { IconSymbol } from '@/components/ui/IconSymbol';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
|
||||
return (
|
||||
<ThemedView>
|
||||
<TouchableOpacity
|
||||
style={styles.heading}
|
||||
onPress={() => setIsOpen((value) => !value)}
|
||||
activeOpacity={0.8}>
|
||||
<IconSymbol
|
||||
name="chevron.right"
|
||||
size={18}
|
||||
weight="medium"
|
||||
color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
|
||||
style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }}
|
||||
/>
|
||||
|
||||
<ThemedText type="defaultSemiBold">{title}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
{isOpen && <ThemedView style={styles.content}>{children}</ThemedView>}
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
heading: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
content: {
|
||||
marginTop: 6,
|
||||
marginLeft: 24,
|
||||
},
|
||||
});
|
||||
@@ -13,9 +13,7 @@ export function ExternalLink({ href, ...rest }: Props) {
|
||||
href={href}
|
||||
onPress={async (event) => {
|
||||
if (Platform.OS !== 'web') {
|
||||
// Prevent the default behavior of linking to the default browser on native.
|
||||
event.preventDefault();
|
||||
// Open the link in an in-app browser.
|
||||
await openBrowserAsync(href);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
|
||||
export function HelloWave() {
|
||||
const rotationAnimation = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
rotationAnimation.value = withRepeat(
|
||||
withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
|
||||
4 // Run the animation 4 times
|
||||
);
|
||||
}, [rotationAnimation]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${rotationAnimation.value}deg` }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedStyle}>
|
||||
<ThemedText style={styles.text}>👋</ThemedText>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
text: {
|
||||
fontSize: 28,
|
||||
lineHeight: 32,
|
||||
marginTop: -6,
|
||||
},
|
||||
});
|
||||
72
components/PageContainer.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Platform, ScrollView, StyleSheet } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
|
||||
type PageContainerProps = PropsWithChildren<{
|
||||
scrollable?: boolean;
|
||||
style?: any;
|
||||
}>;
|
||||
|
||||
export function PageContainer({ children, scrollable = true, style }: PageContainerProps) {
|
||||
let insets;
|
||||
try {
|
||||
insets = useSafeAreaInsets();
|
||||
} catch {
|
||||
insets = {
|
||||
top: Platform.OS === 'ios' ? 44 : 24,
|
||||
bottom: Platform.OS === 'ios' ? 34 : 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const containerStyle = [
|
||||
styles.container,
|
||||
{
|
||||
paddingTop: insets.top + 20,
|
||||
},
|
||||
style
|
||||
];
|
||||
|
||||
const contentStyle = [
|
||||
styles.contentContainer,
|
||||
{
|
||||
paddingBottom: insets.bottom + 20,
|
||||
}
|
||||
];
|
||||
|
||||
if (scrollable) {
|
||||
return (
|
||||
<ThemedView style={containerStyle}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={contentStyle}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedView style={[containerStyle, contentStyle]}>
|
||||
{children}
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 20,
|
||||
},
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Animated, {
|
||||
interpolate,
|
||||
useAnimatedRef,
|
||||
useAnimatedStyle,
|
||||
useScrollViewOffset,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { useBottomTabOverflow } from '@/components/ui/TabBarBackground';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
const HEADER_HEIGHT = 250;
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
headerImage: ReactElement;
|
||||
headerBackgroundColor: { dark: string; light: string };
|
||||
}>;
|
||||
|
||||
export default function ParallaxScrollView({
|
||||
children,
|
||||
headerImage,
|
||||
headerBackgroundColor,
|
||||
}: Props) {
|
||||
const colorScheme = useColorScheme() ?? 'light';
|
||||
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
||||
const scrollOffset = useScrollViewOffset(scrollRef);
|
||||
const bottom = useBottomTabOverflow();
|
||||
const headerAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(
|
||||
scrollOffset.value,
|
||||
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
|
||||
[-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
|
||||
),
|
||||
},
|
||||
{
|
||||
scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<Animated.ScrollView
|
||||
ref={scrollRef}
|
||||
scrollEventThrottle={16}
|
||||
scrollIndicatorInsets={{ bottom }}
|
||||
contentContainerStyle={{ paddingBottom: bottom }}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.header,
|
||||
{ backgroundColor: headerBackgroundColor[colorScheme] },
|
||||
headerAnimatedStyle,
|
||||
]}>
|
||||
{headerImage}
|
||||
</Animated.View>
|
||||
<ThemedView style={styles.content}>{children}</ThemedView>
|
||||
</Animated.ScrollView>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
height: HEADER_HEIGHT,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 32,
|
||||
gap: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
36
components/QueryProvider.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Retry failed requests 3 times
|
||||
retry: 3,
|
||||
// Retry after 1 second
|
||||
retryDelay: 1000,
|
||||
// Stale time of 5 minutes
|
||||
staleTime: 5 * 60 * 1000,
|
||||
// Cache time of 10 minutes
|
||||
gcTime: 10 * 60 * 1000,
|
||||
},
|
||||
mutations: {
|
||||
// Retry failed mutations 1 time
|
||||
retry: 1,
|
||||
// Retry after 2 seconds
|
||||
retryDelay: 2000,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
interface QueryProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function QueryProvider({ children }: QueryProviderProps) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
53
components/profile/ProfilePicture.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Image, ImageStyle, View, ViewStyle } from 'react-native';
|
||||
|
||||
export type ProfilePictureProps = {
|
||||
source: string | { uri: string };
|
||||
size?: number;
|
||||
borderWidth?: number;
|
||||
borderColor?: string;
|
||||
style?: ViewStyle;
|
||||
imageStyle?: ImageStyle;
|
||||
};
|
||||
|
||||
export function ProfilePicture({
|
||||
source,
|
||||
size = 40,
|
||||
borderWidth = 2,
|
||||
borderColor = '#FFFFFF',
|
||||
style,
|
||||
imageStyle,
|
||||
}: ProfilePictureProps) {
|
||||
const containerStyle: ViewStyle = {
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
borderWidth,
|
||||
borderColor,
|
||||
overflow: 'hidden',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
};
|
||||
|
||||
const imageContainerStyle: ImageStyle = {
|
||||
width: size - (borderWidth * 2),
|
||||
height: size - (borderWidth * 2),
|
||||
borderRadius: (size - (borderWidth * 2)) / 2,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[containerStyle, style]}>
|
||||
<Image
|
||||
source={typeof source === 'string' ? { uri: source } : source}
|
||||
style={[imageContainerStyle, imageStyle]}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
322
components/profile/ProfileScreen.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useFocusEffect, useRouter } from "expo-router";
|
||||
import * as WebBrowser from 'expo-web-browser';
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { Alert, StyleSheet } from "react-native";
|
||||
|
||||
import { useLinkedInProfileAndStatus } from "@/api/LinkedInAPI";
|
||||
import { useProfile } from "@/api/ProfileAPI";
|
||||
import { PageContainer } from "@/components/PageContainer";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
import { ProfilePicture } from "@/components/profile/ProfilePicture";
|
||||
import { ThemedButton } from "@/components/ui/ThemedButton";
|
||||
import { ThemedCard } from "@/components/ui/ThemedCard";
|
||||
import { AuthContext } from "@/contexts/AuthContext";
|
||||
import { ConnectionStatus, connectLinkedInUser } from "@/helper/linkedinConnect";
|
||||
|
||||
export default function ProfileScreen({ userId }: { userId: string }) {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const authContext = useContext(AuthContext);
|
||||
const currentUserId = authContext?.user?.id;
|
||||
const { data: profile, isLoading, error, refetch: refetchProfile } = useProfile(userId);
|
||||
const { data: currentUserProfile, refetch: refetchCurrentUserProfile } = useProfile(currentUserId || '');
|
||||
|
||||
const { data: linkedInData, isLoading: linkedInLoading, refetch: refetchLinkedInData } = useLinkedInProfileAndStatus(
|
||||
profile?.linkedin_profile_id || '',
|
||||
currentUserProfile?.unipile_id || '',
|
||||
!!(profile?.linkedin_profile_id && currentUserProfile?.unipile_id)
|
||||
);
|
||||
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// Refetch data whenever the screen comes into focus
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (userId) {
|
||||
refetchProfile();
|
||||
}
|
||||
|
||||
if (currentUserId) {
|
||||
refetchCurrentUserProfile();
|
||||
}
|
||||
|
||||
if (profile?.linkedin_profile_id && currentUserProfile?.unipile_id) {
|
||||
refetchLinkedInData();
|
||||
}
|
||||
}, [userId, currentUserId, profile?.linkedin_profile_id, currentUserProfile?.unipile_id, refetchProfile, refetchCurrentUserProfile, refetchLinkedInData])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
if (profile?.linkedin_profile_id && currentUserProfile?.unipile_id) {
|
||||
refetchLinkedInData();
|
||||
}
|
||||
}, [profile?.linkedin_profile_id, currentUserProfile?.unipile_id, refetchLinkedInData]);
|
||||
|
||||
const connectionStatus: ConnectionStatus = linkedInData?.connectionStatus?.status || 'none';
|
||||
const isOwnProfile = profile?.id === currentUserId;
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (!profile?.linkedin_profile_id) {
|
||||
console.error('No LinkedIn profile ID available for target user');
|
||||
Alert.alert(
|
||||
'Connection Error',
|
||||
'This user does not have a LinkedIn profile connected.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current user's unipile_id as the account ID
|
||||
if (!currentUserProfile?.unipile_id) {
|
||||
console.error('No Unipile account ID available for current user');
|
||||
Alert.alert(
|
||||
'Connection Error',
|
||||
'Your LinkedIn account is not properly connected. Please reconnect your LinkedIn account.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConnecting(true);
|
||||
|
||||
try {
|
||||
const result = await connectLinkedInUser({
|
||||
linkedinProfileId: profile.linkedin_profile_id,
|
||||
accountId: currentUserProfile.unipile_id
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// Invalidate the LinkedIn profile status query to force a refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['linkedin-profile-status', profile.linkedin_profile_id, currentUserProfile.unipile_id]
|
||||
});
|
||||
|
||||
Alert.alert(
|
||||
'Connection Request Sent',
|
||||
'Your LinkedIn connection request has been sent successfully!',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Connection Failed',
|
||||
`Failed to send connection request: ${result.error}`,
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during connection process:', error);
|
||||
Alert.alert(
|
||||
'Connection Error',
|
||||
'An unexpected error occurred while trying to connect. Please try again.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewLinkedInProfile = async () => {
|
||||
if (profile?.linkedin_profile_id) {
|
||||
try {
|
||||
const linkedInUrl = `https://www.linkedin.com/in/${profile.linkedin_profile_id}`;
|
||||
await WebBrowser.openBrowserAsync(linkedInUrl);
|
||||
} catch (error) {
|
||||
console.error('Error opening LinkedIn profile:', error);
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Unable to open LinkedIn profile. Please try again.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Alert.alert(
|
||||
'LinkedIn Profile Unavailable',
|
||||
'This user\'s LinkedIn profile is not available.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageContainer style={styles.pageContainer} scrollable={false}>
|
||||
<ThemedText>Loading profile...</ThemedText>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<PageContainer style={styles.pageContainer} scrollable={false}>
|
||||
<ThemedText>Error loading profile: {error.message}</ThemedText>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return (
|
||||
<PageContainer style={styles.pageContainer} scrollable={false}>
|
||||
<ThemedText>Profile not found</ThemedText>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer style={styles.pageContainer} scrollable={false}>
|
||||
<ThemedView style={styles.headerContainer}>
|
||||
<ProfilePicture
|
||||
source={`https://api.dicebear.com/9.x/thumbs/png?seed=${profile.id}`}
|
||||
size={80}
|
||||
/>
|
||||
<ThemedText type="title" style={{ textAlign: "center" }}>
|
||||
{profile.display_name || "Unknown User"}
|
||||
</ThemedText>
|
||||
<ThemedText type="defaultSemiBold" style={{ textAlign: "center", color: "gray" }}>
|
||||
@{profile.username}
|
||||
</ThemedText>
|
||||
{(profile.id !== currentUserId && profile.linkedin_connected !== false) && (
|
||||
<ThemedView style={styles.connectionContainer}>
|
||||
{connectionStatus === 'none' && (
|
||||
<ThemedButton
|
||||
title={isConnecting ? "Connecting..." : "Connect"}
|
||||
onPress={handleConnect}
|
||||
variant="outline"
|
||||
size="medium"
|
||||
icon={isConnecting ? undefined : "link"}
|
||||
iconPosition="left"
|
||||
disabled={isConnecting || linkedInLoading}
|
||||
loading={isConnecting}
|
||||
/>
|
||||
)}
|
||||
{connectionStatus === 'pending' && (
|
||||
<ThemedButton
|
||||
title="Pending Connection"
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
'Connection Status',
|
||||
'You have already sent a connection request to this user. It is currently pending.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
}}
|
||||
variant="filled"
|
||||
size="medium"
|
||||
icon="clock.fill"
|
||||
iconPosition="left"
|
||||
customBackgroundColor="white"
|
||||
customTextColor="black"
|
||||
/>
|
||||
)}
|
||||
{connectionStatus === 'connected' && (
|
||||
<ThemedButton
|
||||
title="Connected"
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
'Connection Status',
|
||||
'You are already connected with this user on LinkedIn.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
}}
|
||||
variant="filled"
|
||||
size="medium"
|
||||
icon="checkmark.circle.fill"
|
||||
iconPosition="left"
|
||||
customBackgroundColor="#0077B5"
|
||||
customTextColor="white"
|
||||
/>
|
||||
)}
|
||||
</ThemedView>
|
||||
)}
|
||||
</ThemedView>
|
||||
{profile.linkedin_connected === false && !isOwnProfile && (
|
||||
<ThemedCard
|
||||
backgroundColor="tint"
|
||||
style={styles.linkedinCard}
|
||||
padding="medium"
|
||||
>
|
||||
<ThemedText style={styles.linkedinCardText}>
|
||||
This user is not connected to LinkedIn
|
||||
</ThemedText>
|
||||
</ThemedCard>
|
||||
)}
|
||||
<ThemedText style={{ textAlign: "center" }}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
|
||||
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
|
||||
commodo consequat.
|
||||
</ThemedText>
|
||||
|
||||
{(profile.linkedin_connected !== false) && (
|
||||
<ThemedView style={styles.linkedInButtonContainer}>
|
||||
<ThemedButton
|
||||
title="View LinkedIn Profile"
|
||||
onPress={handleViewLinkedInProfile}
|
||||
variant="filled"
|
||||
size="medium"
|
||||
icon="link"
|
||||
iconPosition="left"
|
||||
customBackgroundColor="#0077B5"
|
||||
customTextColor="white"
|
||||
/>
|
||||
</ThemedView>
|
||||
)}
|
||||
|
||||
{/* Back button for non-own profiles */}
|
||||
{!isOwnProfile && (
|
||||
<ThemedView style={styles.backButtonContainer}>
|
||||
<ThemedButton
|
||||
title="Back"
|
||||
onPress={() => router.push('/(tabs)/profiles')}
|
||||
variant="outline"
|
||||
size="medium"
|
||||
icon="chevron.left"
|
||||
iconPosition="left"
|
||||
/>
|
||||
</ThemedView>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pageContainer: {
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
headerContainer: {
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
marginBottom: 24,
|
||||
},
|
||||
linkedinCard: {
|
||||
marginBottom: 16,
|
||||
width: "100%",
|
||||
maxWidth: 300,
|
||||
},
|
||||
linkedinCardText: {
|
||||
textAlign: "center",
|
||||
color: "#fff",
|
||||
fontWeight: "500",
|
||||
},
|
||||
connectionContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
},
|
||||
backButtonContainer: {
|
||||
marginTop: 16,
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
linkedInButtonContainer: {
|
||||
marginTop: 16,
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,23 +1,69 @@
|
||||
// Fallback for using MaterialIcons on Android and web.
|
||||
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
|
||||
import { SymbolWeight } from 'expo-symbols';
|
||||
import { ComponentProps } from 'react';
|
||||
import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
|
||||
|
||||
type IconMapping = Record<SymbolViewProps['name'], ComponentProps<typeof MaterialIcons>['name']>;
|
||||
type IconSymbolName = keyof typeof MAPPING;
|
||||
// Define a more flexible type for our custom mappings
|
||||
type IconMapping = Record<string, ComponentProps<typeof MaterialIcons>['name']>;
|
||||
export type IconSymbolName = keyof typeof MAPPING;
|
||||
|
||||
/**
|
||||
* Add your SF Symbols to Material Icons mappings here.
|
||||
* - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
|
||||
* - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
|
||||
*/
|
||||
const MAPPING = {
|
||||
export const MAPPING = {
|
||||
'house.fill': 'home',
|
||||
'paperplane.fill': 'send',
|
||||
'chevron.left.forwardslash.chevron.right': 'code',
|
||||
'chevron.right': 'chevron-right',
|
||||
'chevron.left': 'chevron-left',
|
||||
'person.fill': 'person',
|
||||
'gear': 'settings',
|
||||
'magnifyingglass': 'search',
|
||||
'heart.fill': 'favorite',
|
||||
'star.fill': 'star',
|
||||
'bookmark.fill': 'bookmark',
|
||||
'bell.fill': 'notifications',
|
||||
'envelope.fill': 'email',
|
||||
'phone.fill': 'phone',
|
||||
'camera.fill': 'camera-alt',
|
||||
'photo.fill': 'photo',
|
||||
'video.fill': 'videocam',
|
||||
'mic.fill': 'mic',
|
||||
'location.fill': 'location-on',
|
||||
'calendar': 'calendar-today',
|
||||
'clock.fill': 'schedule',
|
||||
'checkmark.circle.fill': 'check-circle',
|
||||
'xmark.circle.fill': 'cancel',
|
||||
'plus.circle.fill': 'add-circle',
|
||||
'minus.circle.fill': 'remove-circle',
|
||||
'arrow.up': 'keyboard-arrow-up',
|
||||
'arrow.down': 'keyboard-arrow-down',
|
||||
'arrow.left': 'keyboard-arrow-left',
|
||||
'arrow.right': 'keyboard-arrow-right',
|
||||
'share': 'share',
|
||||
'link': 'link',
|
||||
'link.badge.plus': 'link',
|
||||
'link.badge.minus': 'link-off',
|
||||
'external.link': 'open-in-new',
|
||||
'link.icloud': 'cloud-sync',
|
||||
'download': 'download',
|
||||
'upload': 'upload',
|
||||
'trash.fill': 'delete',
|
||||
'pencil': 'edit',
|
||||
'square.and.pencil': 'edit',
|
||||
'folder.fill': 'folder',
|
||||
'doc.fill': 'description',
|
||||
'list.bullet': 'list',
|
||||
'grid': 'grid-view',
|
||||
'slider.horizontal.3': 'tune',
|
||||
'info.circle.fill': 'info',
|
||||
'questionmark.circle.fill': 'help',
|
||||
'exclamationmark.triangle.fill': 'warning',
|
||||
'rectangle.portrait.and.arrow.right': 'logout',
|
||||
} as IconMapping;
|
||||
|
||||
/**
|
||||
|
||||
172
components/ui/ThemedButton.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, TextStyle, TouchableOpacity, ViewStyle } from 'react-native';
|
||||
import { IconSymbol, IconSymbolName } from './IconSymbol';
|
||||
|
||||
export type ThemedButtonVariant = 'filled' | 'outline' | 'ghost';
|
||||
export type ThemedButtonSize = 'small' | 'medium' | 'large';
|
||||
|
||||
export type ThemedButtonProps = {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
variant?: ThemedButtonVariant;
|
||||
size?: ThemedButtonSize;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
icon?: IconSymbolName;
|
||||
iconPosition?: 'left' | 'right';
|
||||
customBackgroundColor?: string;
|
||||
customTextColor?: string;
|
||||
customBorderColor?: string;
|
||||
style?: ViewStyle;
|
||||
textStyle?: TextStyle;
|
||||
};
|
||||
|
||||
export function ThemedButton({
|
||||
title,
|
||||
onPress,
|
||||
variant = 'filled',
|
||||
size = 'medium',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
icon,
|
||||
iconPosition = 'left',
|
||||
customBackgroundColor,
|
||||
customTextColor,
|
||||
customBorderColor,
|
||||
style,
|
||||
textStyle,
|
||||
}: ThemedButtonProps) {
|
||||
const tintColor = useThemeColor({}, 'tint');
|
||||
const textColor = useThemeColor({}, 'text');
|
||||
|
||||
const getSizeStyles = (): { padding: number; fontSize: number; iconSize: number } => {
|
||||
switch (size) {
|
||||
case 'small':
|
||||
return { padding: 8, fontSize: 14, iconSize: 16 };
|
||||
case 'large':
|
||||
return { padding: 16, fontSize: 18, iconSize: 20 };
|
||||
default:
|
||||
return { padding: 12, fontSize: 16, iconSize: 18 };
|
||||
}
|
||||
};
|
||||
|
||||
const { padding, fontSize, iconSize } = getSizeStyles();
|
||||
|
||||
const getButtonStyles = (): ViewStyle => {
|
||||
const baseStyle: ViewStyle = {
|
||||
paddingHorizontal: padding * 1.5,
|
||||
paddingVertical: padding,
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: size === 'small' ? 36 : size === 'large' ? 52 : 44,
|
||||
};
|
||||
|
||||
switch (variant) {
|
||||
case 'filled':
|
||||
return {
|
||||
...baseStyle,
|
||||
backgroundColor: customBackgroundColor || tintColor,
|
||||
};
|
||||
case 'outline':
|
||||
return {
|
||||
...baseStyle,
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: customBorderColor || tintColor,
|
||||
};
|
||||
case 'ghost':
|
||||
return {
|
||||
...baseStyle,
|
||||
backgroundColor: 'transparent',
|
||||
};
|
||||
default:
|
||||
return baseStyle;
|
||||
}
|
||||
};
|
||||
|
||||
const getTextStyles = (): TextStyle => {
|
||||
const baseStyle: TextStyle = {
|
||||
fontSize,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
switch (variant) {
|
||||
case 'filled':
|
||||
return {
|
||||
...baseStyle,
|
||||
color: customTextColor || '#FFFFFF',
|
||||
};
|
||||
case 'outline':
|
||||
return {
|
||||
...baseStyle,
|
||||
color: customTextColor || tintColor,
|
||||
};
|
||||
case 'ghost':
|
||||
return {
|
||||
...baseStyle,
|
||||
color: customTextColor || tintColor,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...baseStyle,
|
||||
color: customTextColor || textColor,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const buttonStyle = getButtonStyles();
|
||||
const textStyleObj = getTextStyles();
|
||||
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
buttonStyle,
|
||||
isDisabled && styles.disabled,
|
||||
style,
|
||||
]}
|
||||
onPress={onPress}
|
||||
disabled={isDisabled}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{icon && iconPosition === 'left' && (
|
||||
<IconSymbol
|
||||
name={icon}
|
||||
size={iconSize}
|
||||
color={String(textStyleObj.color || textColor)}
|
||||
style={styles.leftIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text style={[textStyleObj, textStyle]}>
|
||||
{loading ? 'Loading...' : title}
|
||||
</Text>
|
||||
|
||||
{icon && iconPosition === 'right' && (
|
||||
<IconSymbol
|
||||
name={icon}
|
||||
size={iconSize}
|
||||
color={String(textStyleObj.color || textColor)}
|
||||
style={styles.rightIcon}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
leftIcon: {
|
||||
marginRight: 8,
|
||||
},
|
||||
rightIcon: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
61
components/ui/ThemedCard.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { View, type ViewProps } from 'react-native';
|
||||
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
|
||||
export type ThemedCardProps = ViewProps & {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
backgroundColor?: keyof typeof import('@/constants/Colors').Colors.light;
|
||||
padding?: number | 'small' | 'medium' | 'large';
|
||||
borderRadius?: number;
|
||||
elevation?: number;
|
||||
shadowColor?: string;
|
||||
};
|
||||
|
||||
export function ThemedCard({
|
||||
style,
|
||||
lightColor,
|
||||
darkColor,
|
||||
backgroundColor,
|
||||
padding = 'medium',
|
||||
borderRadius = 12,
|
||||
elevation = 2,
|
||||
shadowColor,
|
||||
children,
|
||||
...otherProps
|
||||
}: ThemedCardProps) {
|
||||
const cardBackgroundColor = backgroundColor
|
||||
? useThemeColor({}, backgroundColor)
|
||||
: useThemeColor({ light: lightColor, dark: darkColor }, 'background');
|
||||
|
||||
const defaultShadowColor = useThemeColor({}, 'text');
|
||||
|
||||
const getPaddingValue = () => {
|
||||
switch (padding) {
|
||||
case 'small': return 12;
|
||||
case 'medium': return 16;
|
||||
case 'large': return 20;
|
||||
default: return typeof padding === 'number' ? padding : 16;
|
||||
}
|
||||
};
|
||||
|
||||
const cardStyle = {
|
||||
backgroundColor: cardBackgroundColor,
|
||||
borderRadius,
|
||||
padding: getPaddingValue(),
|
||||
shadowColor: shadowColor || defaultShadowColor,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: elevation,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: elevation * 2,
|
||||
elevation,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[cardStyle, style]} {...otherProps}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
|
||||
*/
|
||||
|
||||
const tintColorLight = '#0a7ea4';
|
||||
const tintColorDark = '#fff';
|
||||
const tintColorLight = '#26a6d4';
|
||||
const tintColorDark = '#26a6d4';
|
||||
|
||||
export const Colors = {
|
||||
light: {
|
||||
@@ -14,6 +14,7 @@ export const Colors = {
|
||||
icon: '#687076',
|
||||
tabIconDefault: '#687076',
|
||||
tabIconSelected: tintColorLight,
|
||||
cardBackground: '#d3d7db',
|
||||
},
|
||||
dark: {
|
||||
text: '#ECEDEE',
|
||||
@@ -22,5 +23,6 @@ export const Colors = {
|
||||
icon: '#9BA1A6',
|
||||
tabIconDefault: '#9BA1A6',
|
||||
tabIconSelected: tintColorDark,
|
||||
cardBackground: '#474444',
|
||||
},
|
||||
};
|
||||
|
||||
123
contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { ProfileAPI } from '@/api/ProfileAPI'
|
||||
import { Profile } from '@/types/profile'
|
||||
import { supabase } from '@/utils/supabase'
|
||||
import { User } from '@supabase/supabase-js'
|
||||
import { createContext, ReactNode, useEffect, useState } from "react"
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null
|
||||
profile: Profile | null
|
||||
profileLoading: boolean
|
||||
login: (email: string, password: string) => Promise<void>
|
||||
logout: () => Promise<void>
|
||||
register: (email: string, password: string, profileData: { username: string; display_name: string }) => Promise<void>
|
||||
refreshProfile: () => Promise<void>
|
||||
}
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
|
||||
export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [profile, setProfile] = useState<Profile | null>(null)
|
||||
const [profileLoading, setProfileLoading] = useState(true)
|
||||
|
||||
// Fetch user profile
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setProfileLoading(true);
|
||||
ProfileAPI.getCurrentUserProfile()
|
||||
.then(setProfile)
|
||||
.catch(console.error)
|
||||
.finally(() => setProfileLoading(false));
|
||||
} else {
|
||||
setProfile(null);
|
||||
setProfileLoading(false);
|
||||
}
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setUser(session?.user ?? null)
|
||||
})
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
async (event, session) => {
|
||||
setUser(session?.user ?? null)
|
||||
}
|
||||
)
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [])
|
||||
|
||||
async function login(email: string, password: string) {
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Login failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function register(email: string, password: string, profileData: { username: string; display_name: string }) {
|
||||
// Create user account on supabase
|
||||
const { data: authData, error: authError } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
if (authError) {
|
||||
throw new Error(`Registration failed: ${authError.message}`)
|
||||
}
|
||||
|
||||
if (authData.user) {
|
||||
// Create profile entry in supabase
|
||||
try {
|
||||
await ProfileAPI.createProfile({
|
||||
id: authData.user.id,
|
||||
email: email,
|
||||
username: profileData.username,
|
||||
display_name: profileData.display_name,
|
||||
})
|
||||
} catch (profileError) {
|
||||
throw new Error(`Profile creation failed: ${profileError instanceof Error ? profileError.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
const { error } = await supabase.auth.signOut()
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Logout failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshProfile() {
|
||||
if (user) {
|
||||
setProfileLoading(true);
|
||||
try {
|
||||
const newProfile = await ProfileAPI.getCurrentUserProfile()
|
||||
setProfile(newProfile)
|
||||
} catch (error) {
|
||||
console.error('Error refreshing profile:', error)
|
||||
} finally {
|
||||
setProfileLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
user, profile, profileLoading, login, logout, register, refreshProfile,
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
21
eas.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 16.11.0",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
90
helper/linkedinConnect.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { LinkedInAPI } from '@/api/LinkedInAPI'
|
||||
|
||||
export interface ConnectLinkedInUserParams {
|
||||
linkedinProfileId: string
|
||||
accountId: string
|
||||
}
|
||||
|
||||
export interface ConnectLinkedInUserResult {
|
||||
success: boolean
|
||||
invitationId?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type ConnectionStatus = 'connected' | 'pending' | 'none' | 'error'
|
||||
|
||||
export interface ConnectionStatusResult {
|
||||
status: ConnectionStatus
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the connection status between the current user and a target LinkedIn profile
|
||||
* based on the profile data returned from getProfileByIdentifier
|
||||
*/
|
||||
export function getConnectionStatusFromProfile(profile: any): ConnectionStatusResult {
|
||||
try {
|
||||
// If there's a relationship, they are connected
|
||||
if (profile.is_relationship) {
|
||||
return { status: 'connected' }
|
||||
}
|
||||
|
||||
// If there's an invitation object, check its status
|
||||
if (profile.invitation) {
|
||||
if (profile.invitation.type === 'SENT' && profile.invitation.status === 'PENDING') {
|
||||
return { status: 'pending' }
|
||||
}
|
||||
}
|
||||
|
||||
// If no relationship and no pending invitation, show connect button
|
||||
return { status: 'none' }
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: error?.message || 'Error determining connection status'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to connect with a LinkedIn user through a 3-step process:
|
||||
* 1. Get the profile's LinkedIn public identifier from the linkedin_profile_id
|
||||
* 2. Call the API to get the provider ID
|
||||
* 3. Send the connection request
|
||||
*/
|
||||
|
||||
export async function connectLinkedInUser({
|
||||
linkedinProfileId,
|
||||
accountId
|
||||
}: ConnectLinkedInUserParams): Promise<ConnectLinkedInUserResult> {
|
||||
try {
|
||||
const publicIdentifier = linkedinProfileId;
|
||||
|
||||
//Call the API to get the provider ID
|
||||
const profile = await LinkedInAPI.getProfileByIdentifier(publicIdentifier, accountId);
|
||||
|
||||
if (!profile || !profile.provider_id) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to retrieve LinkedIn profile or provider ID not found'
|
||||
}
|
||||
}
|
||||
|
||||
//Send the connection request
|
||||
const invitation = await LinkedInAPI.sendInvitation({
|
||||
provider_id: profile.provider_id,
|
||||
account_id: accountId
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
invitationId: invitation.invitation_id
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error?.message || 'Unknown error occurred during connection process'
|
||||
}
|
||||
}
|
||||
}
|
||||
634
package-lock.json
generated
@@ -9,10 +9,13 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||
"@react-navigation/elements": "^2.3.8",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"expo": "~53.0.11",
|
||||
"@supabase/supabase-js": "^2.50.0",
|
||||
"@tanstack/react-query": "^5.80.7",
|
||||
"expo": "53.0.12",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-constants": "~17.1.6",
|
||||
"expo-font": "~13.3.1",
|
||||
@@ -23,15 +26,16 @@
|
||||
"expo-splash-screen": "~0.30.9",
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-symbols": "~0.4.5",
|
||||
"expo-system-ui": "~5.0.8",
|
||||
"expo-web-browser": "~14.1.6",
|
||||
"expo-system-ui": "~5.0.9",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-native": "0.79.3",
|
||||
"react-native": "0.79.4",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-webview": "13.13.5"
|
||||
},
|
||||
@@ -40,6 +44,7 @@
|
||||
"@types/react": "~19.0.10",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-config-expo": "~9.2.0",
|
||||
"supabase": "^2.26.9",
|
||||
"typescript": "~5.8.3"
|
||||
}
|
||||
},
|
||||
@@ -1734,29 +1739,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/cli": {
|
||||
"version": "0.24.14",
|
||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.14.tgz",
|
||||
"integrity": "sha512-o+QYyfIBhSRTgaywKTLJhm2Fg5PrSeUVCXS+uQySamgoMjLNhHa8QwE64mW/FmJr5hZLiqUEQxb60FK4JcyqXg==",
|
||||
"version": "0.24.15",
|
||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.15.tgz",
|
||||
"integrity": "sha512-RDZS30OSnbXkSPnBXdyPL29KbltjOmegE23bZZDiGV23WOReWcPgRc5U7Fd8eLPhtRjHBKlBpNJMTed5Ntr/uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@0no-co/graphql.web": "^1.0.8",
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"@expo/code-signing-certificates": "^0.0.5",
|
||||
"@expo/config": "~11.0.10",
|
||||
"@expo/config-plugins": "~10.0.2",
|
||||
"@expo/config-plugins": "~10.0.3",
|
||||
"@expo/devcert": "^1.1.2",
|
||||
"@expo/env": "~1.0.5",
|
||||
"@expo/image-utils": "^0.7.4",
|
||||
"@expo/json-file": "^9.1.4",
|
||||
"@expo/metro-config": "~0.20.14",
|
||||
"@expo/metro-config": "~0.20.15",
|
||||
"@expo/osascript": "^2.2.4",
|
||||
"@expo/package-manager": "^1.8.4",
|
||||
"@expo/plist": "^0.3.4",
|
||||
"@expo/prebuild-config": "^9.0.6",
|
||||
"@expo/prebuild-config": "^9.0.7",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/ws-tunnel": "^1.0.1",
|
||||
"@expo/xcpretty": "^4.3.0",
|
||||
"@react-native/dev-middleware": "0.79.3",
|
||||
"@react-native/dev-middleware": "0.79.4",
|
||||
"@urql/core": "^5.0.6",
|
||||
"@urql/exchange-retry": "^1.3.0",
|
||||
"accepts": "^1.3.8",
|
||||
@@ -1873,18 +1878,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/config-plugins": {
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-10.0.2.tgz",
|
||||
"integrity": "sha512-TzUn3pPdpwCS0yYaSlZOClgDmCX8N4I2lfgitX5oStqmvpPtB+vqtdyqsVM02fQ2tlJIAqwBW+NHaHqqy8Jv7g==",
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-10.0.3.tgz",
|
||||
"integrity": "sha512-fjCckkde67pSDf48x7wRuPsgQVIqlDwN7NlOk9/DFgQ1hCH0L5pGqoSmikA1vtAyiA83MOTpkGl3F3wyATyUog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/config-types": "^53.0.3",
|
||||
"@expo/config-types": "^53.0.4",
|
||||
"@expo/json-file": "~9.1.4",
|
||||
"@expo/plist": "^0.3.4",
|
||||
"@expo/sdk-runtime-versions": "^1.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"debug": "^4.3.5",
|
||||
"getenv": "^1.0.0",
|
||||
"getenv": "^2.0.0",
|
||||
"glob": "^10.4.2",
|
||||
"resolve-from": "^5.0.0",
|
||||
"semver": "^7.5.4",
|
||||
@@ -1894,15 +1899,6 @@
|
||||
"xml2js": "0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/config-plugins/node_modules/getenv": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz",
|
||||
"integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/config-plugins/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
@@ -1994,9 +1990,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/fingerprint": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.13.0.tgz",
|
||||
"integrity": "sha512-3IwpH0p3uO8jrJSLOUNDzJVh7VEBod0emnCBq0hD72sy6ICmzauM6Xf4he+2Tip7fzImCJRd63GaehV+CCtpvA==",
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.13.1.tgz",
|
||||
"integrity": "sha512-MgZ5uIvvwAnjWeQoj4D3RnBXjD1GNOpCvhp2jtZWdQ8yEokhDEJGoHjsMT8/NCB5m2fqP5sv2V5nPzC7CN1YjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
@@ -2005,6 +2001,7 @@
|
||||
"debug": "^4.3.4",
|
||||
"find-up": "^5.0.0",
|
||||
"getenv": "^2.0.0",
|
||||
"glob": "^10.4.2",
|
||||
"ignore": "^5.3.1",
|
||||
"minimatch": "^9.0.0",
|
||||
"p-limit": "^3.1.0",
|
||||
@@ -2109,16 +2106,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/metro-config": {
|
||||
"version": "0.20.14",
|
||||
"resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.20.14.tgz",
|
||||
"integrity": "sha512-tYDDubuZycK+NX00XN7BMu73kBur/evOPcKfxc+UBeFfgN2EifOITtdwSUDdRsbtJ2OnXwMY1HfRUG3Lq3l4cw==",
|
||||
"version": "0.20.15",
|
||||
"resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.20.15.tgz",
|
||||
"integrity": "sha512-m8i58IQ7I8iOdVRfOhFmhPMHuhgeTVfQp1+mxW7URqPZaeVbuDVktPqOiNoHraKBoGPLKMUSsD+qdUuJVL3wMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@babel/generator": "^7.20.5",
|
||||
"@babel/parser": "^7.20.0",
|
||||
"@babel/types": "^7.20.0",
|
||||
"@expo/config": "~11.0.9",
|
||||
"@expo/config": "~11.0.10",
|
||||
"@expo/env": "~1.0.5",
|
||||
"@expo/json-file": "~9.1.4",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
@@ -2126,7 +2123,7 @@
|
||||
"debug": "^4.3.2",
|
||||
"dotenv": "~16.4.5",
|
||||
"dotenv-expand": "~11.0.6",
|
||||
"getenv": "^1.0.0",
|
||||
"getenv": "^2.0.0",
|
||||
"glob": "^10.4.2",
|
||||
"jsc-safe-url": "^0.2.4",
|
||||
"lightningcss": "~1.27.0",
|
||||
@@ -2144,15 +2141,6 @@
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/metro-config/node_modules/getenv": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz",
|
||||
"integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/metro-config/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
@@ -2216,17 +2204,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/prebuild-config": {
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.6.tgz",
|
||||
"integrity": "sha512-HDTdlMkTQZ95rd6EpvuLM+xkZV03yGLc38FqI37qKFLJtUN1WnYVaWsuXKoljd1OrVEVsHe6CfqKwaPZ52D56Q==",
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.7.tgz",
|
||||
"integrity": "sha512-1w5MBp6NdF51gPGp0HsCZt0QC82hZWo37wI9HfxhdQF/sN/92Mh4t30vaY7gjHe71T5QNyab00oxZH/wP0MDgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/config": "~11.0.9",
|
||||
"@expo/config-plugins": "~10.0.2",
|
||||
"@expo/config": "~11.0.10",
|
||||
"@expo/config-plugins": "~10.0.3",
|
||||
"@expo/config-types": "^53.0.4",
|
||||
"@expo/image-utils": "^0.7.4",
|
||||
"@expo/json-file": "^9.1.4",
|
||||
"@react-native/normalize-colors": "0.79.2",
|
||||
"@react-native/normalize-colors": "0.79.4",
|
||||
"debug": "^4.3.1",
|
||||
"resolve-from": "^5.0.0",
|
||||
"semver": "^7.6.0",
|
||||
@@ -2815,32 +2803,44 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-async-storage/async-storage": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz",
|
||||
"integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"merge-options": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/assets-registry": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.3.tgz",
|
||||
"integrity": "sha512-Vy8DQXCJ21YSAiHxrNBz35VqVlZPpRYm50xRTWRf660JwHuJkFQG8cUkrLzm7AUriqUXxwpkQHcY+b0ibw9ejQ==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.4.tgz",
|
||||
"integrity": "sha512-7PjHNRtYlc36B7P1PHme8ZV0ZJ/xsA/LvMoXe6EX++t7tSPJ8iYCMBryZhcdnztgce73b94Hfx6TTGbLF+xtUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/babel-plugin-codegen": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.3.tgz",
|
||||
"integrity": "sha512-Zb8F4bSEKKZfms5n1MQ0o5mudDcpAINkKiFuFTU0PErYGjY3kZ+JeIP+gS6KCXsckxCfMEKQwqKicP/4DWgsZQ==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.4.tgz",
|
||||
"integrity": "sha512-quhytIlDedR3ircRwifa22CaWVUVnkxccrrgztroCZaemSJM+HLurKJrjKWm0J5jV9ed+d+9Qyb1YB0syTHDjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.25.3",
|
||||
"@react-native/codegen": "0.79.3"
|
||||
"@react-native/codegen": "0.79.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/babel-preset": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.3.tgz",
|
||||
"integrity": "sha512-VHGNP02bDD2Ul1my0pLVwe/0dsEBHxR343ySpgnkCNEEm9C1ANQIL2wvnJrHZPcqfAkWfFQ8Ln3t+6fdm4A/Dg==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.4.tgz",
|
||||
"integrity": "sha512-El9JvYKiNfnkQ3qR7zJvvRdP3DX2i4BGYlIricWQishI3gWAfm88FQYFC2CcGoMQWJQEPN4jnDMpoISAJDEN4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
@@ -2884,7 +2884,7 @@
|
||||
"@babel/plugin-transform-typescript": "^7.25.2",
|
||||
"@babel/plugin-transform-unicode-regex": "^7.24.7",
|
||||
"@babel/template": "^7.25.0",
|
||||
"@react-native/babel-plugin-codegen": "0.79.3",
|
||||
"@react-native/babel-plugin-codegen": "0.79.4",
|
||||
"babel-plugin-syntax-hermes-parser": "0.25.1",
|
||||
"babel-plugin-transform-flow-enums": "^0.0.2",
|
||||
"react-refresh": "^0.14.0"
|
||||
@@ -2897,9 +2897,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/codegen": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.3.tgz",
|
||||
"integrity": "sha512-CZejXqKch/a5/s/MO5T8mkAgvzCXgsTkQtpCF15kWR9HN8T+16k0CsN7TXAxXycltoxiE3XRglOrZNEa/TiZUQ==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.4.tgz",
|
||||
"integrity": "sha512-K0moZDTJtqZqSs+u9tnDPSxNsdxi5irq8Nu4mzzOYlJTVNGy5H9BiIDg/NeKGfjAdo43yTDoaPSbUCvVV8cgIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.1",
|
||||
@@ -2937,12 +2937,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/community-cli-plugin": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.3.tgz",
|
||||
"integrity": "sha512-N/+p4HQqN4yK6IRzn7OgMvUIcrmEWkecglk1q5nj+AzNpfIOzB+mqR20SYmnPfeXF+mZzYCzRANb3KiM+WsSDA==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.4.tgz",
|
||||
"integrity": "sha512-lx1RXEJwU9Tcs2B2uiDZBa6yghU6m6STvwYqHbJlFZyNN1k3JRa9j0/CDu+0fCFacIn7rEfZpb4UWi5YhsHpQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-native/dev-middleware": "0.79.3",
|
||||
"@react-native/dev-middleware": "0.79.4",
|
||||
"chalk": "^4.0.0",
|
||||
"debug": "^2.2.0",
|
||||
"invariant": "^2.2.4",
|
||||
@@ -2991,22 +2991,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/debugger-frontend": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.3.tgz",
|
||||
"integrity": "sha512-ImNDuEeKH6lEsLXms3ZsgIrNF94jymfuhPcVY5L0trzaYNo9ZFE9Ni2/18E1IbfXxdeIHrCSBJlWD6CTm7wu5A==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.4.tgz",
|
||||
"integrity": "sha512-Gg4LhxHIK86Bi2RiT1rbFAB6fuwANRsaZJ1sFZ1OZEMQEx6stEnzaIrmfgzcv4z0bTQdQ8lzCrpsz0qtdaD4eA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/dev-middleware": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.3.tgz",
|
||||
"integrity": "sha512-x88+RGOyG71+idQefnQg7wLhzjn/Scs+re1O5vqCkTVzRAc/f7SdHMlbmECUxJPd08FqMcOJr7/X3nsJBrNuuw==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.4.tgz",
|
||||
"integrity": "sha512-OWRDNkgrFEo+OSC5QKfiiBmGXKoU8gmIABK8rj2PkgwisFQ/22p7MzE5b6oB2lxWaeJT7jBX5KVniNqO46VhHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/ttlcache": "^1.4.1",
|
||||
"@react-native/debugger-frontend": "0.79.3",
|
||||
"@react-native/debugger-frontend": "0.79.4",
|
||||
"chrome-launcher": "^0.15.2",
|
||||
"chromium-edge-launcher": "^0.2.0",
|
||||
"connect": "^3.6.5",
|
||||
@@ -3046,29 +3046,52 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/gradle-plugin": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.79.3.tgz",
|
||||
"integrity": "sha512-imfpZLhNBc9UFSzb/MOy2tNcIBHqVmexh/qdzw83F75BmUtLb/Gs1L2V5gw+WI1r7RqDILbWk7gXB8zUllwd+g==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.79.4.tgz",
|
||||
"integrity": "sha512-Gv5ryy23k7Sib2xVgqw65GTryg9YTij6URcMul5cI7LRcW0Aa1/FPb26l388P4oeNGNdDoAkkS+CuCWNunRuWg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/js-polyfills": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.79.3.tgz",
|
||||
"integrity": "sha512-PEBtg6Kox6KahjCAch0UrqCAmHiNLEbp2SblUEoFAQnov4DSxBN9safh+QSVaCiMAwLjvNfXrJyygZz60Dqz3Q==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.79.4.tgz",
|
||||
"integrity": "sha512-VyKPo/l9zP4+oXpQHrJq4vNOtxF7F5IMdQmceNzTnRpybRvGGgO/9jYu9mdmdKRO2KpQEc5dB4W2rYhVKdGNKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/normalize-colors": {
|
||||
"version": "0.79.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.2.tgz",
|
||||
"integrity": "sha512-+b+GNrupWrWw1okHnEENz63j7NSMqhKeFMOyzYLBwKcprG8fqJQhDIGXfizKdxeIa5NnGSAevKL1Ev1zJ56X8w==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.4.tgz",
|
||||
"integrity": "sha512-247/8pHghbYY2wKjJpUsY6ZNbWcdUa5j5517LZMn6pXrbSSgWuj3JA4OYibNnocCHBaVrt+3R8XC3VEJqLlHFg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-native/virtualized-lists": {
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.4.tgz",
|
||||
"integrity": "sha512-0Mdcox6e5PTonuM1WIo3ks7MBAa3IDzj0pKnE5xAwSgQ0DJW2P5dYf+KjWmpkE+Yb0w41ZbtXPhKq+U2JJ6C/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4",
|
||||
"nullthrows": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/bottom-tabs": {
|
||||
"version": "7.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.3.15.tgz",
|
||||
@@ -3200,6 +3223,106 @@
|
||||
"@sinonjs/commons": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.70.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.70.0.tgz",
|
||||
"integrity": "sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz",
|
||||
"integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch": {
|
||||
"version": "2.6.15",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "1.19.4",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz",
|
||||
"integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.10.tgz",
|
||||
"integrity": "sha512-SJKVa7EejnuyfImrbzx+HaD9i6T784khuw1zP+MBD7BmJYChegGxYigPzkKX8CK8nGuDntmeSD3fvriaH0EGZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.13",
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
|
||||
"integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz",
|
||||
"integrity": "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.70.0",
|
||||
"@supabase/functions-js": "2.4.4",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "1.19.4",
|
||||
"@supabase/realtime-js": "2.11.10",
|
||||
"@supabase/storage-js": "2.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.80.7",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.7.tgz",
|
||||
"integrity": "sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.80.7",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.7.tgz",
|
||||
"integrity": "sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.80.7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
||||
@@ -3320,11 +3443,17 @@
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.14.tgz",
|
||||
"integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -3336,6 +3465,15 @@
|
||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
||||
@@ -4474,9 +4612,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-expo": {
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.2.0.tgz",
|
||||
"integrity": "sha512-oNUeUZPMNRPmx/2jaKJLSQFP/MFI1M91vP+Gp+j8/FPl9p/ps603DNwCaRdcT/Vj3FfREdlIwRio1qDCjY0oAA==",
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.2.1.tgz",
|
||||
"integrity": "sha512-Ol3w0uLJNQ5tDfCf4L+IDTDMgJkVMQHhvYqMxs18Ib0DcaBQIfE8mneSSk7FcuI6FS0phw/rZhoEquQh1/Q3wA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.25.9",
|
||||
@@ -4493,7 +4631,7 @@
|
||||
"@babel/plugin-transform-runtime": "^7.24.7",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.23.0",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/babel-preset": "0.79.4",
|
||||
"babel-plugin-react-native-web": "~0.19.13",
|
||||
"babel-plugin-syntax-hermes-parser": "^0.25.1",
|
||||
"babel-plugin-transform-flow-enums": "^0.0.2",
|
||||
@@ -4590,6 +4728,47 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/bin-links": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bin-links/-/bin-links-5.0.0.tgz",
|
||||
"integrity": "sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cmd-shim": "^7.0.0",
|
||||
"npm-normalize-package-bin": "^4.0.0",
|
||||
"proc-log": "^5.0.0",
|
||||
"read-cmd-shim": "^5.0.0",
|
||||
"write-file-atomic": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bin-links/node_modules/proc-log": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
|
||||
"integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bin-links/node_modules/write-file-atomic": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz",
|
||||
"integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bplist-creator": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||
@@ -4992,6 +5171,16 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cmd-shim": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-7.0.0.tgz",
|
||||
"integrity": "sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
@@ -5255,9 +5444,19 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
||||
@@ -6207,25 +6406,25 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expo": {
|
||||
"version": "53.0.11",
|
||||
"resolved": "https://registry.npmjs.org/expo/-/expo-53.0.11.tgz",
|
||||
"integrity": "sha512-+QtvU+6VPd7/o4vmtwuRE/Li2rAiJtD25I6BOnoQSxphaWWaD0PdRQnIV3VQ0HESuJYRuKJ3DkAHNJ3jI6xwzA==",
|
||||
"version": "53.0.12",
|
||||
"resolved": "https://registry.npmjs.org/expo/-/expo-53.0.12.tgz",
|
||||
"integrity": "sha512-dtmED749hkxDWCcvtD++tb8bAm3Twv8qnUOXzVyXA5owNG0mwDIz0HveJTpWK1UzkY4HcTVRezDf0tflZJ+JXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"@expo/cli": "0.24.14",
|
||||
"@expo/cli": "0.24.15",
|
||||
"@expo/config": "~11.0.10",
|
||||
"@expo/config-plugins": "~10.0.2",
|
||||
"@expo/fingerprint": "0.13.0",
|
||||
"@expo/metro-config": "0.20.14",
|
||||
"@expo/config-plugins": "~10.0.3",
|
||||
"@expo/fingerprint": "0.13.1",
|
||||
"@expo/metro-config": "0.20.15",
|
||||
"@expo/vector-icons": "^14.0.0",
|
||||
"babel-preset-expo": "~13.2.0",
|
||||
"babel-preset-expo": "~13.2.1",
|
||||
"expo-asset": "~11.1.5",
|
||||
"expo-constants": "~17.1.6",
|
||||
"expo-file-system": "~18.1.10",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-keep-awake": "~14.1.4",
|
||||
"expo-modules-autolinking": "2.1.11",
|
||||
"expo-modules-autolinking": "2.1.12",
|
||||
"expo-modules-core": "2.4.0",
|
||||
"react-native-edge-to-edge": "1.6.0",
|
||||
"whatwg-url-without-unicode": "8.0.0-3"
|
||||
@@ -6368,9 +6567,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expo-modules-autolinking": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.11.tgz",
|
||||
"integrity": "sha512-KrWQo+cE4gWYNePBBhmHGVzf63gYV19ZLXe9EIH3GHTkViVzIX+Lp618H/7GxfawpN5kbhvilATH1QEKKnUUww==",
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.12.tgz",
|
||||
"integrity": "sha512-rW5YSW66pUx1nLqn7TO0eWRnP4LDvySW1Tom0wjexk3Tx/upg9LYE5tva7p5AX/cdFfiZcEqPcOxP4RyT++Xlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
@@ -6488,12 +6687,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/expo-system-ui": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-5.0.8.tgz",
|
||||
"integrity": "sha512-2sI7ALq3W8sKKa3FRW7PmuNznk+48cb1VzFy96vYZLZgTDZViz+fEJNdp1RHgLui/mAl3f8md1LneygSJvZ1EQ==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/expo-system-ui/-/expo-system-ui-5.0.9.tgz",
|
||||
"integrity": "sha512-behQ4uP384++U9GjgXmyEno1Z8raN1/tZv7ZJhYkQkRj1g2xXvNltXN/PDRcOm/wCNqStVhDpYo2OOQm5E5/lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-native/normalize-colors": "0.79.3",
|
||||
"@react-native/normalize-colors": "0.79.4",
|
||||
"debug": "^4.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -6507,16 +6706,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo-system-ui/node_modules/@react-native/normalize-colors": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.3.tgz",
|
||||
"integrity": "sha512-T75NIQPRFCj6DFMxtcVMJTZR+3vHXaUMSd15t+CkJpc5LnyX91GVaPxpRSAdjFh7m3Yppl5MpdjV/fntImheYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expo-web-browser": {
|
||||
"version": "14.1.6",
|
||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.1.6.tgz",
|
||||
"integrity": "sha512-/4P8eWqRyfXIMZna3acg320LXNA+P2cwyEVbjDX8vHnWU+UnOtyRKWy3XaAIyMPQ9hVjBNUQTh4MPvtnPRzakw==",
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.2.0.tgz",
|
||||
"integrity": "sha512-6S51d8pVlDRDsgGAp8BPpwnxtyKiMWEFdezNz+5jVIyT+ctReW42uxnjRgtsdn5sXaqzhaX+Tzk/CWaKCyC0hw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
@@ -6658,6 +6851,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -6806,6 +7023,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/freeport-async": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz",
|
||||
@@ -7725,6 +7955,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-obj": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@@ -8711,6 +8950,18 @@
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-options": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
||||
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-plain-obj": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@@ -9256,6 +9507,27 @@
|
||||
"integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
@@ -9306,6 +9578,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-normalize-package-bin": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
|
||||
"integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-package-arg": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz",
|
||||
@@ -10205,19 +10487,19 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.3.tgz",
|
||||
"integrity": "sha512-EzH1+9gzdyEo9zdP6u7Sh3Jtf5EOMwzy+TK65JysdlgAzfEVfq4mNeXcAZ6SmD+CW6M7ARJbvXLyTD0l2S5rpg==",
|
||||
"version": "0.79.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.4.tgz",
|
||||
"integrity": "sha512-CfxYMuszvnO/33Q5rB//7cU1u9P8rSOvzhE2053Phdb8+6bof9NLayCllU2nmPrm8n9o6RU1Fz5H0yquLQ0DAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/create-cache-key-function": "^29.7.0",
|
||||
"@react-native/assets-registry": "0.79.3",
|
||||
"@react-native/codegen": "0.79.3",
|
||||
"@react-native/community-cli-plugin": "0.79.3",
|
||||
"@react-native/gradle-plugin": "0.79.3",
|
||||
"@react-native/js-polyfills": "0.79.3",
|
||||
"@react-native/normalize-colors": "0.79.3",
|
||||
"@react-native/virtualized-lists": "0.79.3",
|
||||
"@react-native/assets-registry": "0.79.4",
|
||||
"@react-native/codegen": "0.79.4",
|
||||
"@react-native/community-cli-plugin": "0.79.4",
|
||||
"@react-native/gradle-plugin": "0.79.4",
|
||||
"@react-native/js-polyfills": "0.79.4",
|
||||
"@react-native/normalize-colors": "0.79.4",
|
||||
"@react-native/virtualized-lists": "0.79.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"anser": "^1.4.9",
|
||||
"ansi-regex": "^5.0.0",
|
||||
@@ -10348,6 +10630,18 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-url-polyfill": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz",
|
||||
"integrity": "sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url-without-unicode": "8.0.0-3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-web": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz",
|
||||
@@ -10394,35 +10688,6 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/@react-native/normalize-colors": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.3.tgz",
|
||||
"integrity": "sha512-T75NIQPRFCj6DFMxtcVMJTZR+3vHXaUMSd15t+CkJpc5LnyX91GVaPxpRSAdjFh7m3Yppl5MpdjV/fntImheYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
|
||||
"version": "0.79.3",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.3.tgz",
|
||||
"integrity": "sha512-/0rRozkn+iIHya2vnnvprDgT7QkfI54FLrACAN3BLP7MRlfOIGOrZsXpRLndnLBVnjNzkcre84i1RecjoXnwIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4",
|
||||
"nullthrows": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
@@ -10483,6 +10748,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cmd-shim": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-5.0.0.tgz",
|
||||
"integrity": "sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||
@@ -11761,6 +12036,45 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/supabase": {
|
||||
"version": "2.26.9",
|
||||
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.26.9.tgz",
|
||||
"integrity": "sha512-wHl7HtAD2iHMVXL8JZyfSjdI0WYM7EF0ydThp1tSvDANaD2JHCZc8GH1NdzglbwGqdHmjCYeSZ+H28fmucYl7Q==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bin-links": "^5.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"tar": "7.4.3"
|
||||
},
|
||||
"bin": {
|
||||
"supabase": "bin/supabase"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supabase/node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -11865,9 +12179,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.43.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.0.tgz",
|
||||
"integrity": "sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==",
|
||||
"version": "5.43.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
|
||||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
@@ -12469,6 +12783,16 @@
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
|
||||
26
package.json
@@ -5,17 +5,21 @@
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"lint": "expo lint"
|
||||
"lint": "expo lint",
|
||||
"preinstall": "node preinstall.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||
"@react-navigation/elements": "^2.3.8",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"expo": "~53.0.11",
|
||||
"@supabase/supabase-js": "^2.50.0",
|
||||
"@tanstack/react-query": "^5.80.7",
|
||||
"expo": "53.0.12",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-constants": "~17.1.6",
|
||||
"expo-font": "~13.3.1",
|
||||
@@ -26,24 +30,26 @@
|
||||
"expo-splash-screen": "~0.30.9",
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-symbols": "~0.4.5",
|
||||
"expo-system-ui": "~5.0.8",
|
||||
"expo-web-browser": "~14.1.6",
|
||||
"expo-system-ui": "~5.0.9",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-native": "0.79.3",
|
||||
"react-native": "0.79.4",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-webview": "13.13.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/react": "~19.0.10",
|
||||
"typescript": "~5.8.3",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-config-expo": "~9.2.0"
|
||||
"eslint-config-expo": "~9.2.0",
|
||||
"supabase": "^2.26.9",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
1
preinstall.js
Normal file
1
supabase/.temp/cli-latest
Normal file
@@ -0,0 +1 @@
|
||||
v2.26.9
|
||||
22
supabase/config.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
[functions.get-unipile-key]
|
||||
enabled = true
|
||||
verify_jwt = true
|
||||
import_map = "./functions/get-unipile-key/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
entrypoint = "./functions/get-unipile-key/index.ts"
|
||||
# Specifies static files to be bundled with the function. Supports glob patterns.
|
||||
# For example, if you want to serve static HTML pages in your function:
|
||||
# static_files = [ "./functions/get-unipile-key/*.html" ]
|
||||
|
||||
[functions.unipile-webhook]
|
||||
enabled = true
|
||||
verify_jwt = true
|
||||
import_map = "./functions/unipile-webhook/deno.json"
|
||||
# Uncomment to specify a custom file path to the entrypoint.
|
||||
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
|
||||
entrypoint = "./functions/unipile-webhook/index.ts"
|
||||
# Specifies static files to be bundled with the function. Supports glob patterns.
|
||||
# For example, if you want to serve static HTML pages in your function:
|
||||
# static_files = [ "./functions/unipile-webhook/*.html" ]
|
||||
3
supabase/functions/get-unipile-key/.npmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
# Configuration for private npm package dependencies
|
||||
# For more information on using private registries with Edge Functions, see:
|
||||
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
|
||||
3
supabase/functions/get-unipile-key/deno.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"imports": {}
|
||||
}
|
||||
14
supabase/functions/get-unipile-key/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
|
||||
|
||||
serve(async (_req: Request) => {
|
||||
const apiKey = Deno.env.get("UNIPILE_API_KEY");
|
||||
|
||||
if (!apiKey) {
|
||||
return new Response("API key not found", { status: 500 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ apiKey }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
});
|
||||
3
supabase/functions/unipile-webhook/.npmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
# Configuration for private npm package dependencies
|
||||
# For more information on using private registries with Edge Functions, see:
|
||||
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
|
||||
3
supabase/functions/unipile-webhook/deno.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"imports": {}
|
||||
}
|
||||
119
supabase/functions/unipile-webhook/index.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// @ts-nocheck
|
||||
import { serve } from "https://deno.land/std/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
const supabase = createClient(
|
||||
Deno.env.get("SUPABASE_URL")!,
|
||||
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
|
||||
);
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method !== "POST") {
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await req.json();
|
||||
console.log("Received webhook:", JSON.stringify(body));
|
||||
|
||||
// Ignore webhooks with AccountStatus objects
|
||||
if (body?.AccountStatus) {
|
||||
console.log("Ignoring webhook with AccountStatus object");
|
||||
return new Response("Ignoring AccountStatus webhook", { status: 200 });
|
||||
}
|
||||
|
||||
const status = body?.status;
|
||||
const accountId = body?.account_id;
|
||||
const supabaseUserId = body?.name;
|
||||
|
||||
console.log("Parsed webhook data:", { status, accountId, supabaseUserId });
|
||||
|
||||
if (!status || !accountId || !supabaseUserId) {
|
||||
console.error("Missing required fields:", { status, accountId, supabaseUserId });
|
||||
return new Response("Missing required fields", { status: 400 });
|
||||
}
|
||||
|
||||
// Only process successful connections
|
||||
if (status !== "CREATION_SUCCESS" && status !== "RECONNECTED") {
|
||||
console.log("Ignoring webhook with status:", status);
|
||||
return new Response("Status not handled", { status: 200 });
|
||||
}
|
||||
|
||||
const { data: updatedProfile, error } = await supabase
|
||||
.from("profiles")
|
||||
.update({
|
||||
linkedin_connected: true,
|
||||
unipile_id: accountId,
|
||||
})
|
||||
.eq("id", supabaseUserId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error("Update error:", error);
|
||||
return new Response("Failed to update LinkedIn connection", { status: 500 });
|
||||
}
|
||||
|
||||
console.log("Successfully updated profile:", updatedProfile);
|
||||
|
||||
// Fetch account details from Unipile to get LinkedIn profile ID
|
||||
try {
|
||||
const unipileApiKey = Deno.env.get("UNIPILE_API_KEY");
|
||||
if (!unipileApiKey) {
|
||||
console.error("UNIPILE_API_KEY not found in environment");
|
||||
return new Response("Missing Unipile API key", { status: 500 });
|
||||
}
|
||||
|
||||
const unipileResponse = await fetch(`https://api14.unipile.com:14496/api/v1/accounts/${accountId}`, {
|
||||
headers: {
|
||||
"X-API-KEY": unipileApiKey,
|
||||
"accept": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!unipileResponse.ok) {
|
||||
console.error("Failed to fetch account details from Unipile:", unipileResponse.status);
|
||||
return new Response("Failed to fetch account details", { status: 500 });
|
||||
}
|
||||
|
||||
const accountDetails = await unipileResponse.json();
|
||||
console.log("Account details from Unipile:", JSON.stringify(accountDetails));
|
||||
|
||||
const linkedinProfileId = accountDetails?.connection_params?.im?.publicIdentifier;
|
||||
|
||||
if (linkedinProfileId) {
|
||||
// Update the user's linkedin_profile_id
|
||||
const { error: linkedinUpdateError } = await supabase
|
||||
.from("profiles")
|
||||
.update({
|
||||
linkedin_profile_id: linkedinProfileId,
|
||||
})
|
||||
.eq("id", supabaseUserId);
|
||||
|
||||
if (linkedinUpdateError) {
|
||||
console.error("Failed to update LinkedIn profile ID:", linkedinUpdateError);
|
||||
return new Response("Failed to update LinkedIn profile ID", { status: 500 });
|
||||
}
|
||||
|
||||
console.log("Successfully updated LinkedIn profile ID:", linkedinProfileId);
|
||||
} else {
|
||||
console.warn("LinkedIn profile ID not found in account details");
|
||||
}
|
||||
} catch (unipileError) {
|
||||
console.error("Error fetching account details from Unipile:", unipileError);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
accountId,
|
||||
userId: supabaseUserId
|
||||
}), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
status: 200,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error("Webhook error:", err);
|
||||
return new Response("Invalid JSON body", { status: 400 });
|
||||
}
|
||||
});
|
||||
29
types/profile.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export interface Profile {
|
||||
id: string
|
||||
updated_at?: string | null
|
||||
username?: string | null
|
||||
email?: string | null
|
||||
display_name?: string | null
|
||||
linkedin_connected?: boolean | null
|
||||
linkedin_profile_id?: string | null
|
||||
unipile_id?: string | null
|
||||
}
|
||||
|
||||
export interface CreateProfileData {
|
||||
id: string
|
||||
username?: string
|
||||
email?: string
|
||||
display_name?: string
|
||||
linkedin_connected?: boolean
|
||||
linkedin_profile_id?: string
|
||||
unipile_id?: string
|
||||
}
|
||||
|
||||
export interface UpdateProfileData {
|
||||
username?: string
|
||||
email?: string
|
||||
display_name?: string
|
||||
linkedin_connected?: boolean
|
||||
linkedin_profile_id?: string
|
||||
unipile_id?: string
|
||||
}
|
||||
16
utils/supabase.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import 'react-native-url-polyfill/auto'
|
||||
|
||||
export const supabase = createClient(
|
||||
process.env.EXPO_PUBLIC_SUPABASE_URL || "",
|
||||
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || "",
|
||||
{
|
||||
auth: {
|
||||
storage: AsyncStorage,
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
})
|
||||
|
||||
45
utils/unipile.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const UNIPILE_CONFIG = {
|
||||
BASE_URL: "https://api14.unipile.com:14496",
|
||||
LINKEDIN_ENDPOINT: "/linkedin",
|
||||
DSN: "api14.unipile.com:14496",
|
||||
}
|
||||
|
||||
export const getUnipileHeaders = (apiKey: string) => ({
|
||||
"X-API-KEY": apiKey,
|
||||
"accept": "application/json",
|
||||
"content-type": "application/json",
|
||||
});
|
||||
|
||||
export async function fetchUnipileApiKeyFromSupabase(): Promise<string> {
|
||||
const response = await fetch('https://rzzhsadiatfvgyiqdjmd.supabase.co/functions/v1/get-unipile-key', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Unipile API key from Supabase Edge Function');
|
||||
}
|
||||
const data = await response.json();
|
||||
if (!data || !data.apiKey) {
|
||||
throw new Error('UNIPILE_API_KEY not found in Supabase response');
|
||||
}
|
||||
return data.apiKey;
|
||||
}
|
||||
|
||||
let cachedApiKey: string | null = null;
|
||||
|
||||
export async function getCachedUnipileApiKey(): Promise<string> {
|
||||
if (cachedApiKey) return cachedApiKey;
|
||||
cachedApiKey = await fetchUnipileApiKeyFromSupabase();
|
||||
return cachedApiKey;
|
||||
}
|
||||
|
||||
export async function unipileFetch(input: RequestInfo, init?: RequestInit) {
|
||||
const apiKey = await getCachedUnipileApiKey();
|
||||
const headers = {
|
||||
...getUnipileHeaders(apiKey),
|
||||
...(init?.headers || {}),
|
||||
};
|
||||
return fetch(input, { ...init, headers });
|
||||
}
|
||||