diff --git a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png
index 29ed05b..4438ec3 100644
Binary files a/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
index 1e3a378..1ed3f86 100644
Binary files a/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
index 6b7ddf9..72b74fd 100644
Binary files a/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
index b977b3f..f2b2882 100644
Binary files a/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
index a79fe8b..dda7ae6 100644
Binary files a/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png and b/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
index 4f55492..90eae32 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
index acff384..13f0a54 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
index 68b2b6b..739fbfc 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
index 7ce788c..925188e 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
index 695874a..8f03753 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
index de65e82..2476542 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
index f48cd8b..0303110 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
index b9693cb..f32047c 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
index d48403b..bc419a2 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
index eebf7ad..0bb4297 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
index 5f71916..1bf77f3 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
index 4f6de84..92407f1 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
index bee06ef..578225f 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
index 4208368..cf53f52 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
index 34afa47..7f31cf8 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png
index d5a9df5..302b45f 100644
Binary files a/assets/adaptive-icon.png and b/assets/adaptive-icon.png differ
diff --git a/assets/android/ic_launcher-web.png b/assets/android/ic_launcher-web.png
index 2efccc3..70a03a8 100644
Binary files a/assets/android/ic_launcher-web.png and b/assets/android/ic_launcher-web.png differ
diff --git a/assets/android/mipmap-hdpi/ic_launcher.png b/assets/android/mipmap-hdpi/ic_launcher.png
index af5bd9d..38ab336 100644
Binary files a/assets/android/mipmap-hdpi/ic_launcher.png and b/assets/android/mipmap-hdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-hdpi/ic_launcher_foreground.png b/assets/android/mipmap-hdpi/ic_launcher_foreground.png
index 4aaa708..940dccd 100644
Binary files a/assets/android/mipmap-hdpi/ic_launcher_foreground.png and b/assets/android/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-hdpi/ic_launcher_round.png b/assets/android/mipmap-hdpi/ic_launcher_round.png
index ce446f7..66437aa 100644
Binary files a/assets/android/mipmap-hdpi/ic_launcher_round.png and b/assets/android/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-ldpi/ic_launcher.png b/assets/android/mipmap-ldpi/ic_launcher.png
index 426e2ae..e55a992 100644
Binary files a/assets/android/mipmap-ldpi/ic_launcher.png and b/assets/android/mipmap-ldpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-mdpi/ic_launcher.png b/assets/android/mipmap-mdpi/ic_launcher.png
index c450f86..761b690 100644
Binary files a/assets/android/mipmap-mdpi/ic_launcher.png and b/assets/android/mipmap-mdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-mdpi/ic_launcher_foreground.png b/assets/android/mipmap-mdpi/ic_launcher_foreground.png
index 7ec84d6..e0b31c6 100644
Binary files a/assets/android/mipmap-mdpi/ic_launcher_foreground.png and b/assets/android/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-mdpi/ic_launcher_round.png b/assets/android/mipmap-mdpi/ic_launcher_round.png
index 1185a4d..a627ea0 100644
Binary files a/assets/android/mipmap-mdpi/ic_launcher_round.png and b/assets/android/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-xhdpi/ic_launcher.png b/assets/android/mipmap-xhdpi/ic_launcher.png
index 81d12cd..9007654 100644
Binary files a/assets/android/mipmap-xhdpi/ic_launcher.png and b/assets/android/mipmap-xhdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-xhdpi/ic_launcher_foreground.png b/assets/android/mipmap-xhdpi/ic_launcher_foreground.png
index dbdd835..b41d433 100644
Binary files a/assets/android/mipmap-xhdpi/ic_launcher_foreground.png and b/assets/android/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-xhdpi/ic_launcher_round.png b/assets/android/mipmap-xhdpi/ic_launcher_round.png
index 17ea551..16e1023 100644
Binary files a/assets/android/mipmap-xhdpi/ic_launcher_round.png and b/assets/android/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-xxhdpi/ic_launcher.png b/assets/android/mipmap-xxhdpi/ic_launcher.png
index dd251f4..93d17d5 100644
Binary files a/assets/android/mipmap-xxhdpi/ic_launcher.png and b/assets/android/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png b/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png
index 1a315db..176e8e6 100644
Binary files a/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png and b/assets/android/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-xxhdpi/ic_launcher_round.png b/assets/android/mipmap-xxhdpi/ic_launcher_round.png
index c16b8d9..3e19591 100644
Binary files a/assets/android/mipmap-xxhdpi/ic_launcher_round.png and b/assets/android/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/assets/android/mipmap-xxxhdpi/ic_launcher.png b/assets/android/mipmap-xxxhdpi/ic_launcher.png
index b017bf5..cbdc61f 100644
Binary files a/assets/android/mipmap-xxxhdpi/ic_launcher.png and b/assets/android/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png
index 1c27650..a194108 100644
Binary files a/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png and b/assets/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/assets/android/mipmap-xxxhdpi/ic_launcher_round.png b/assets/android/mipmap-xxxhdpi/ic_launcher_round.png
index 055fe00..0f726c4 100644
Binary files a/assets/android/mipmap-xxxhdpi/ic_launcher_round.png and b/assets/android/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/assets/android/playstore-icon.png b/assets/android/playstore-icon.png
index 8e950cf..f97a895 100644
Binary files a/assets/android/playstore-icon.png and b/assets/android/playstore-icon.png differ
diff --git a/assets/android/values/ic_launcher_background.xml b/assets/android/values/ic_launcher_background.xml
index dcdf032..cb9e160 100644
--- a/assets/android/values/ic_launcher_background.xml
+++ b/assets/android/values/ic_launcher_background.xml
@@ -1,4 +1,4 @@
- #151515
+ #d1d1d2
\ No newline at end of file
diff --git a/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png
index bd6895d..0369553 100644
Binary files a/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png differ
diff --git a/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png
index b6d09cc..0369553 100644
Binary files a/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png differ
diff --git a/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png
index 45de631..0369553 100644
Binary files a/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png differ
diff --git a/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png
index 850078a..0369553 100644
Binary files a/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png differ
diff --git a/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png b/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png
index 5d38cd5..0369553 100644
Binary files a/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png and b/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png differ
diff --git a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f.png b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f.png
index 19e5b3c..0369553 100644
Binary files a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f.png and b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f.png differ
diff --git a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@2x.png b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@2x.png
index 115594f..0369553 100644
Binary files a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@2x.png and b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@2x.png differ
diff --git a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@3x.png b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@3x.png
index 061180d..0369553 100644
Binary files a/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@3x.png and b/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-7d142f.imageset/logo-7d142f@3x.png differ
diff --git a/assets/icon.png b/assets/icon.png
index d5a9df5..0369553 100644
Binary files a/assets/icon.png and b/assets/icon.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png
index ffc8aaa..5837ee4 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png
index 4a52eaa..8fa15ef 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png
index d5eea9b..004058f 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png
index 379634b..4a853b6 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png
index 4ff0ef2..a3e794a 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png
index edcf4d5..3e317c1 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png
index 4a52eaa..8fa15ef 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png
index e4afe63..718e4ca 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png
index 152a8e6..147ca19 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png
index 152a8e6..147ca19 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png b/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png
index bbc2ad2..2faf8a0 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png and b/assets/ios/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png b/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png
index a67bc43..556ef58 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png and b/assets/ios/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png
index 3a87610..1eba090 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 0f242d6..47de330 100644
Binary files a/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/assets/ios/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png b/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png
index e2f386e..5f69402 100644
Binary files a/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png and b/assets/ios/AppIcon.appiconset/ItunesArtwork@2x.png differ
diff --git a/assets/ios/iTunesArtwork@1x.png b/assets/ios/iTunesArtwork@1x.png
index 8e950cf..f97a895 100644
Binary files a/assets/ios/iTunesArtwork@1x.png and b/assets/ios/iTunesArtwork@1x.png differ
diff --git a/assets/ios/iTunesArtwork@2x.png b/assets/ios/iTunesArtwork@2x.png
index e2f386e..5f69402 100644
Binary files a/assets/ios/iTunesArtwork@2x.png and b/assets/ios/iTunesArtwork@2x.png differ
diff --git a/assets/ios/iTunesArtwork@3x.png b/assets/ios/iTunesArtwork@3x.png
index 5c2b758..938011a 100644
Binary files a/assets/ios/iTunesArtwork@3x.png and b/assets/ios/iTunesArtwork@3x.png differ
diff --git a/assets/splash-icon.png b/assets/splash-icon.png
index 5fa6129..0369553 100644
Binary files a/assets/splash-icon.png and b/assets/splash-icon.png differ
diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj
index 1d96622..084418f 100644
--- a/ios/Nuvio.xcodeproj/project.pbxproj
+++ b/ios/Nuvio.xcodeproj/project.pbxproj
@@ -460,7 +460,7 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
- PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app;
+ PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = Nuvio;
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -492,7 +492,7 @@
"-lc++",
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
- PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app;
+ PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
PRODUCT_NAME = Nuvio;
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
SWIFT_VERSION = 5.0;
diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist
index 4ab9b8d..d701baa 100644
--- a/ios/Nuvio/Info.plist
+++ b/ios/Nuvio/Info.plist
@@ -1,98 +1,101 @@
-
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Nuvio
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- CFBundleShortVersionString
- 1.2.5
- CFBundleSignature
- ????
- CFBundleURLTypes
-
-
- CFBundleURLSchemes
-
- nuvio
- com.nuvio.app
-
-
-
- CFBundleURLSchemes
-
- exp+nuvio
-
-
-
- CFBundleVersion
- 20
- LSMinimumSystemVersion
- 12.0
- LSRequiresIPhoneOS
-
- LSSupportsOpeningDocumentsInPlace
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
- NSBonjourServices
-
- _http._tcp
-
- RCTNewArchEnabled
-
- RCTRootViewBackgroundColor
- 4278322180
- UIBackgroundModes
-
- audio
- fetch
-
- UIFileSharingEnabled
-
- UILaunchStoryboardName
- SplashScreen
- UIRequiredDeviceCapabilities
-
- arm64
-
- UIRequiresFullScreen
-
- UIStatusBarStyle
- UIStatusBarStyleDefault
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIUserInterfaceStyle
- Dark
- UIViewControllerBasedStatusBarAppearance
-
-
-
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Nuvio
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.2.5
+ CFBundleSignature
+ ????
+ CFBundleURLTypes
+
+
+ CFBundleURLSchemes
+
+ nuvio
+ com.nuvio.app
+
+
+
+ CFBundleURLSchemes
+
+ exp+nuvio
+
+
+
+ CFBundleVersion
+ 20
+ LSMinimumSystemVersion
+ 12.0
+ LSRequiresIPhoneOS
+
+ LSSupportsOpeningDocumentsInPlace
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ NSBonjourServices
+
+ _http._tcp
+
+ NSLocalNetworkUsageDescription
+ Allow $(PRODUCT_NAME) to access your local network
+ NSMicrophoneUsageDescription
+ This app does not require microphone access.
+ RCTNewArchEnabled
+
+ RCTRootViewBackgroundColor
+ 4278322180
+ UIBackgroundModes
+
+ audio
+
+ UIFileSharingEnabled
+
+ UILaunchStoryboardName
+ SplashScreen
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UIRequiresFullScreen
+
+ UIStatusBarStyle
+ UIStatusBarStyleDefault
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIUserInterfaceStyle
+ Dark
+ UIViewControllerBasedStatusBarAppearance
+
+
+
\ No newline at end of file
diff --git a/ios/Nuvio/NuvioRelease.entitlements b/ios/Nuvio/NuvioRelease.entitlements
index 0c67376..a0bc443 100644
--- a/ios/Nuvio/NuvioRelease.entitlements
+++ b/ios/Nuvio/NuvioRelease.entitlements
@@ -1,5 +1,10 @@
-
-
+
+ aps-environment
+ development
+ com.apple.developer.associated-domains
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 79e673e..2641fdf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@expo/metro-runtime": "~6.1.2",
"@expo/vector-icons": "^15.0.2",
"@gorhom/bottom-sheet": "^5.2.6",
+ "@legendapp/list": "^2.0.13",
"@lottiefiles/dotlottie-react": "^0.6.5",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/blur": "^4.4.1",
@@ -29,7 +30,6 @@
"@react-navigation/stack": "^7.2.10",
"@sentry/react-native": "~7.3.0",
"@shopify/flash-list": "^2.1.0",
- "@supabase/supabase-js": "^2.54.0",
"@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20",
"axios": "^1.12.2",
@@ -67,6 +67,7 @@
"posthog-react-native": "^4.4.0",
"react": "19.1.0",
"react-native": "0.81.4",
+ "react-native-boost": "^0.6.2",
"react-native-bottom-tabs": "^0.12.2",
"react-native-gesture-handler": "~2.28.0",
"react-native-get-random-values": "^1.11.0",
@@ -80,7 +81,7 @@
"react-native-svg": "15.12.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.3.0",
- "react-native-video": "^6.12.0",
+ "react-native-video": "^6.17.0",
"react-native-web": "^0.21.0",
"react-native-wheel-color-picker": "^1.3.1",
"react-native-worklets": "^0.6.1",
@@ -2347,6 +2348,27 @@
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
"license": "MIT"
},
+ "node_modules/@isaacs/balanced-match": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
+ "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@isaacs/brace-expansion": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
+ "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@isaacs/balanced-match": "^4.0.1"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -3636,80 +3658,6 @@
"@sinonjs/commons": "^3.0.0"
}
},
- "node_modules/@supabase/auth-js": {
- "version": "2.75.0",
- "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz",
- "integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15"
- }
- },
- "node_modules/@supabase/functions-js": {
- "version": "2.75.0",
- "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz",
- "integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15"
- }
- },
- "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": "2.75.0",
- "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz",
- "integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15"
- }
- },
- "node_modules/@supabase/realtime-js": {
- "version": "2.75.0",
- "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz",
- "integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15",
- "@types/phoenix": "^1.6.6",
- "@types/ws": "^8.18.1",
- "ws": "^8.18.2"
- }
- },
- "node_modules/@supabase/storage-js": {
- "version": "2.75.0",
- "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz",
- "integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==",
- "license": "MIT",
- "dependencies": {
- "@supabase/node-fetch": "2.6.15"
- }
- },
- "node_modules/@supabase/supabase-js": {
- "version": "2.75.0",
- "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz",
- "integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==",
- "license": "MIT",
- "dependencies": {
- "@supabase/auth-js": "2.75.0",
- "@supabase/functions-js": "2.75.0",
- "@supabase/node-fetch": "2.6.15",
- "@supabase/postgrest-js": "2.75.0",
- "@supabase/realtime-js": "2.75.0",
- "@supabase/storage-js": "2.75.0"
- }
- },
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
@@ -4234,12 +4182,6 @@
"undici-types": "~7.14.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/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -4283,15 +4225,6 @@
"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",
@@ -10759,6 +10692,37 @@
}
}
},
+ "node_modules/react-native-boost": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/react-native-boost/-/react-native-boost-0.6.2.tgz",
+ "integrity": "sha512-6w9PdGvFzyI1dyN516+mLfFF5vETPsjoc26rUFlzWav7PNbC7WV0KyfTBr0q/cDjZkWLMleWQZkGTqSQ1H4PHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.25.0",
+ "@babel/helper-module-imports": "^7.25.0",
+ "@babel/helper-plugin-utils": "^7.25.0",
+ "minimatch": "^10.0.1"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/react-native-boost/node_modules/minimatch": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
+ "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/brace-expansion": "^5.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/react-native-bottom-tabs": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/react-native-bottom-tabs/-/react-native-bottom-tabs-0.12.2.tgz",
diff --git a/package.json b/package.json
index 6c96b1e..f07ab8e 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,6 @@
"@react-navigation/stack": "^7.2.10",
"@sentry/react-native": "~7.3.0",
"@shopify/flash-list": "^2.1.0",
- "@supabase/supabase-js": "^2.54.0",
"@types/lodash": "^4.17.16",
"@types/react-native-video": "^5.0.20",
"axios": "^1.12.2",
diff --git a/src/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx
index f82fff4..a2d3699 100644
--- a/src/contexts/AccountContext.tsx
+++ b/src/contexts/AccountContext.tsx
@@ -1,8 +1,5 @@
import React, { createContext, useContext, useEffect, useMemo, useState, useRef } from 'react';
-import { InteractionManager } from 'react-native';
import accountService, { AuthUser } from '../services/AccountService';
-import supabase from '../services/supabaseClient';
-import syncService from '../services/SyncService';
type AccountContextValue = {
user: AuthUser | null;
@@ -22,73 +19,19 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child
const loadingTimeoutRef = useRef(null);
useEffect(() => {
- // Initial session (load full profile)
- // Defer heavy work until after initial interactions to reduce launch CPU spike
- const task = InteractionManager.runAfterInteractions(() => {
- (async () => {
+ // Initial user load
+ const loadUser = async () => {
+ try {
const u = await accountService.getCurrentUser();
setUser(u);
+ } catch (error) {
+ console.warn('[AccountContext] Failed to load user:', error);
+ } finally {
setLoading(false);
- // Stage sync operations to avoid blocking the JS thread
- syncService.init();
- if (u) {
- try {
- await syncService.migrateLocalScopeToUser();
- // Longer yield to event loop to reduce CPU pressure
- await new Promise(resolve => setTimeout(resolve, 100));
- await syncService.subscribeRealtime();
- await new Promise(resolve => setTimeout(resolve, 100));
- // Pull first to hydrate local state, then push to avoid wiping server with empty local
- await syncService.fullPull();
- await new Promise(resolve => setTimeout(resolve, 100));
- await syncService.fullPush();
- } catch {}
- }
- })();
- });
-
- // Auth state listener
- const { data: subscription } = supabase.auth.onAuthStateChange(async (event, session) => {
- // Only set loading for actual auth changes, not initial session
- if (event !== 'INITIAL_SESSION') {
- setLoading(true);
- }
- try {
- const fullUser = session?.user ? await accountService.getCurrentUser() : null;
- setUser(fullUser);
- // Immediately clear loading so UI can transition to MainTabs/Auth
- setLoading(false);
- if (fullUser) {
- // Run sync in background without blocking UI
- setTimeout(async () => {
- try {
- await syncService.migrateLocalScopeToUser();
- await new Promise(r => setTimeout(r, 0));
- await syncService.subscribeRealtime();
- await new Promise(r => setTimeout(r, 0));
- await syncService.fullPull();
- await new Promise(r => setTimeout(r, 0));
- await syncService.fullPush();
- } catch (error) {
- console.warn('[AccountContext] Background sync failed:', error);
- }
- }, 0);
- } else {
- syncService.unsubscribeRealtime();
- }
- } catch (e) {
- setLoading(false);
- }
- });
-
- return () => {
- subscription.subscription.unsubscribe();
- task.cancel();
- if (loadingTimeoutRef.current) {
- clearTimeout(loadingTimeoutRef.current);
- loadingTimeoutRef.current = null;
}
};
+
+ loadUser();
}, []);
const value = useMemo(() => ({
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
index 7dc62db..45812d4 100644
--- a/src/hooks/useSettings.ts
+++ b/src/hooks/useSettings.ts
@@ -1,5 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
-import { syncService } from '../services/SyncService';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Simple event emitter for settings changes
@@ -230,8 +229,6 @@ export const useSettings = () => {
settingsEmitter.emit();
}
- // If authenticated, push settings to server to prevent overwrite on next pull
- try { syncService.pushSettings(); } catch {}
} catch (error) {
if (__DEV__) console.error('Failed to save settings:', error);
}
diff --git a/src/services/AccountService.ts b/src/services/AccountService.ts
index 029cea1..084af94 100644
--- a/src/services/AccountService.ts
+++ b/src/services/AccountService.ts
@@ -1,5 +1,4 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
-import supabase from './supabaseClient';
export type AuthUser = {
id: string;
@@ -8,6 +7,7 @@ export type AuthUser = {
displayName?: string;
};
+const USER_DATA_KEY = '@user:data';
const USER_SCOPE_KEY = '@user:current';
class AccountService {
@@ -20,53 +20,41 @@ class AccountService {
}
async signUpWithEmail(email: string, password: string): Promise<{ user?: AuthUser; error?: string }> {
- const { data, error } = await supabase.auth.signUp({ email, password });
- if (error) return { error: error.message };
- const user = data.user ? { id: data.user.id, email: data.user.email ?? undefined } : undefined;
- if (user) await AsyncStorage.setItem(USER_SCOPE_KEY, user.id);
- // Initialize profile row
- if (user) {
- await supabase.from('user_profiles').upsert({ user_id: user.id }, { onConflict: 'user_id' });
- }
- return { user };
+ // Since signup is disabled, always return error
+ return { error: 'Sign up is currently disabled due to upcoming system changes' };
}
async signInWithEmail(email: string, password: string): Promise<{ user?: AuthUser; error?: string }> {
- const { data, error } = await supabase.auth.signInWithPassword({ email, password });
- if (error) return { error: error.message };
- const user = data.user ? { id: data.user.id, email: data.user.email ?? undefined } : undefined;
- if (user) await AsyncStorage.setItem(USER_SCOPE_KEY, user.id);
- return { user };
+ // Since signin is disabled, always return error
+ return { error: 'Authentication is currently disabled' };
}
async signOut(): Promise {
- await supabase.auth.signOut();
+ await AsyncStorage.removeItem(USER_DATA_KEY);
await AsyncStorage.setItem(USER_SCOPE_KEY, 'local');
}
async getCurrentUser(): Promise {
- const { data } = await supabase.auth.getUser();
- const u = data.user;
- if (!u) return null;
- // Fetch profile for avatar and display name
- const { data: profile } = await supabase
- .from('user_profiles')
- .select('avatar_url, display_name')
- .eq('user_id', u.id)
- .maybeSingle();
- return { id: u.id, email: u.email ?? undefined, avatarUrl: profile?.avatar_url ?? undefined, displayName: profile?.display_name ?? undefined };
+ try {
+ const userData = await AsyncStorage.getItem(USER_DATA_KEY);
+ if (!userData) return null;
+ return JSON.parse(userData);
+ } catch {
+ return null;
+ }
}
async updateProfile(partial: { avatarUrl?: string; displayName?: string }): Promise {
- const { data } = await supabase.auth.getUser();
- const userId = data.user?.id;
- if (!userId) return 'Not authenticated';
- const { error } = await supabase.from('user_profiles').upsert({
- user_id: userId,
- avatar_url: partial.avatarUrl,
- display_name: partial.displayName,
- }, { onConflict: 'user_id' });
- return error?.message ?? null;
+ try {
+ const currentUser = await this.getCurrentUser();
+ if (!currentUser) return 'Not authenticated';
+
+ const updatedUser = { ...currentUser, ...partial };
+ await AsyncStorage.setItem(USER_DATA_KEY, JSON.stringify(updatedUser));
+ return null;
+ } catch {
+ return 'Failed to update profile';
+ }
}
async getCurrentUserIdScoped(): Promise {
diff --git a/src/services/SyncService.ts b/src/services/SyncService.ts
deleted file mode 100644
index 3c4783a..0000000
--- a/src/services/SyncService.ts
+++ /dev/null
@@ -1,1146 +0,0 @@
-import AsyncStorage from '@react-native-async-storage/async-storage';
-import supabase from './supabaseClient';
-import accountService from './AccountService';
-import { storageService } from './storageService';
-import { addonEmitter, ADDON_EVENTS, stremioService } from './stremioService';
-import { catalogService, StreamingContent } from './catalogService';
-// import localScraperService from './localScraperService';
-import { settingsEmitter } from '../hooks/useSettings';
-import { logger } from '../utils/logger';
-import { traktService } from './traktService';
-
-type WatchProgressRow = {
- user_id: string;
- media_type: string;
- media_id: string;
- episode_id: string;
- current_time_seconds: number;
- duration_seconds: number;
- last_updated_ms: number;
- trakt_synced?: boolean;
- trakt_last_synced_ms?: number | null;
- trakt_progress_percent?: number | null;
-};
-
-const SYNC_QUEUE_KEY = '@sync_queue';
-
-class SyncService {
- private static instance: SyncService;
- private syncing = false;
- private suppressPush = false;
- private realtimeChannels: any[] = [];
- private pullDebounceTimer: NodeJS.Timeout | null = null;
- private addonsPollInterval: NodeJS.Timeout | null = null;
- private suppressLibraryPush: boolean = false;
- private libraryUnsubscribe: (() => void) | null = null;
-
- static getInstance(): SyncService {
- if (!SyncService.instance) SyncService.instance = new SyncService();
- return SyncService.instance;
- }
-
- init(): void {
- // Watch progress updates
- storageService.subscribeToWatchProgressUpdates(() => {
- if (this.suppressPush) return;
- logger.log('[Sync] watch_progress local change → push');
- this.pushWatchProgress().catch(() => undefined);
- });
- storageService.onWatchProgressRemoved((id, type, episodeId) => {
- if (this.suppressPush) return;
- logger.log(`[Sync] watch_progress removed → soft delete ${type}:${id}:${episodeId || ''}`);
- this.softDeleteWatchProgress(type, id, episodeId).catch(() => undefined);
- });
-
- // Addon order and changes
- addonEmitter.on(ADDON_EVENTS.ORDER_CHANGED, () => { logger.log('[Sync] addon order changed → push'); this.pushAddons(); });
- addonEmitter.on(ADDON_EVENTS.ADDON_ADDED, () => { logger.log('[Sync] addon added → push'); this.pushAddons(); });
- addonEmitter.on(ADDON_EVENTS.ADDON_REMOVED, () => { logger.log('[Sync] addon removed → push'); this.pushAddons(); });
-
- // Settings updates: no realtime push; sync only on app restart
- logger.log('[Sync] init completed (listeners wired; settings push disabled)');
-
- // Library local change → push
- if (this.libraryUnsubscribe) {
- try { this.libraryUnsubscribe(); } catch {}
- this.libraryUnsubscribe = null;
- }
- const unsubAdd = catalogService.onLibraryAdd((item) => {
- if (this.suppressLibraryPush) return;
- logger.log(`[Sync] library add → push ${item.type}:${item.id}`);
- this.pushLibraryAdd(item).catch(() => undefined);
- });
- const unsubRem = catalogService.onLibraryRemove((type, id) => {
- if (this.suppressLibraryPush) return;
- logger.log(`[Sync] library remove → push ${type}:${id}`);
- this.pushLibraryRemove(type, id).catch(() => undefined);
- });
- this.libraryUnsubscribe = () => { try { unsubAdd(); unsubRem(); } catch {} };
- }
-
- subscribeRealtime = async (): Promise => {
- const user = await accountService.getCurrentUser();
- if (!user) return;
- const userId = user.id;
- const traktActive = await traktService.isAuthenticated();
-
- const addChannel = (table: string, handler: (payload: any) => void) => {
- const channel = supabase
- .channel(`rt-${table}`)
- .on('postgres_changes', { event: '*', schema: 'public', table, filter: `user_id=eq.${userId}` }, handler)
- .subscribe();
- this.realtimeChannels.push(channel);
- logger.log(`[Sync] Realtime subscribed: ${table}`);
- };
-
- // Watch progress realtime is disabled when Trakt is active
- if (!traktActive) {
- // Watch progress: apply granular updates (ignore self-caused pushes via suppressPush)
- addChannel('watch_progress', async (payload) => {
- try {
- const row = (payload.new || payload.old);
- if (!row) return;
- const type = row.media_type as string;
- const id = row.media_id as string;
- const episodeId = (payload.eventType === 'DELETE') ? (row.episode_id || '') : (row.episode_id || '');
- this.suppressPush = true;
- const deletedAt = (row as any).deleted_at;
- if (payload.eventType === 'DELETE' || deletedAt) {
- await storageService.removeWatchProgress(id, type, episodeId || undefined);
- // Record tombstone with remote timestamp if available
- try {
- const remoteUpdated = (row as any).updated_at ? new Date((row as any).updated_at).getTime() : Date.now();
- await storageService.addWatchProgressTombstone(id, type, episodeId || undefined, remoteUpdated);
- } catch {}
- } else {
- // Preserve the most recent timestamp between local and remote to maintain proper continue watching order
- const remoteTimestamp = row.last_updated_ms || Date.now();
- const existingProgress = await storageService.getWatchProgress(id, type, (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined);
- const localTimestamp = existingProgress?.lastUpdated || 0;
-
- // Use the newer timestamp to maintain proper continue watching order across devices
- const finalTimestamp = Math.max(remoteTimestamp, localTimestamp);
-
- await storageService.setWatchProgress(
- id,
- type,
- {
- currentTime: row.current_time_seconds || 0,
- duration: row.duration_seconds || 0,
- lastUpdated: finalTimestamp,
- traktSynced: row.trakt_synced ?? undefined,
- traktLastSynced: row.trakt_last_synced_ms ?? undefined,
- traktProgress: row.trakt_progress_percent ?? undefined,
- },
- // Ensure we pass through the full remote episode_id as-is; empty string becomes undefined
- (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined,
- { preserveTimestamp: true, forceNotify: true, forceWrite: true }
- );
- }
- } catch {}
- finally {
- this.suppressPush = false;
- }
- });
- } else {
- logger.log('[Sync] Trakt active → skipping watch_progress realtime subscription');
- }
-
- const debouncedPull = (payload?: any) => {
- if (payload?.table) logger.log(`[Sync][rt] change on ${payload.table} → debounced fullPull`);
- if (this.pullDebounceTimer) clearTimeout(this.pullDebounceTimer);
- this.pullDebounceTimer = setTimeout(() => {
- logger.log('[Sync] fullPull (debounced) start');
- this.fullPull()
- .then(() => logger.log('[Sync] fullPull (debounced) done'))
- .catch((e) => { if (__DEV__) console.warn('[Sync] fullPull (debounced) error', e); });
- }, 300);
- };
-
- // Addons: just re-pull snapshot quickly
- addChannel('installed_addons', () => debouncedPull({ table: 'installed_addons' }));
- // Library realtime: apply row-level changes
- addChannel('user_library', async (payload) => {
- try {
- const row = (payload.new || payload.old);
- if (!row) return;
- const mediaType = (row.media_type as string) === 'movie' ? 'movie' : 'series';
- const mediaId = row.media_id as string;
- this.suppressLibraryPush = true;
- const deletedAt = (row as any).deleted_at;
- if (payload.eventType === 'DELETE' || deletedAt) {
- await catalogService.removeFromLibrary(mediaType, mediaId);
- logger.log(`[Sync][rt] user_library DELETE ${mediaType}:${mediaId}`);
- } else {
- const content: StreamingContent = {
- id: mediaId,
- type: mediaType,
- name: (row.title as string) || mediaId,
- poster: (row.poster_url as string) || '',
- inLibrary: true,
- year: row.year ?? undefined,
- } as any;
- await catalogService.addToLibrary(content);
- logger.log(`[Sync][rt] user_library ${payload.eventType} ${mediaType}:${mediaId}`);
- }
- } catch (e) {
- if (__DEV__) console.warn('[Sync][rt] user_library handler error', e);
- } finally {
- this.suppressLibraryPush = false;
- }
- });
- // Excluded: local_scrapers, scraper_repository from realtime sync
- logger.log('[Sync] Realtime subscriptions active');
-
- // Fallback polling for addons (in case realtime isn't enabled)
- if (this.addonsPollInterval) clearInterval(this.addonsPollInterval);
- this.addonsPollInterval = setInterval(async () => {
- try {
- const u = await accountService.getCurrentUser();
- if (!u) return;
- // Compare excluding preinstalled addons
- const exclude = new Set(['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3']);
- const localIds = new Set(
- (await stremioService.getInstalledAddonsAsync())
- .map((a: any) => a.id)
- .filter((id: string) => !exclude.has(id))
- );
- const { data: remote } = await supabase
- .from('installed_addons')
- .select('addon_id')
- .eq('user_id', u.id);
- const remoteIds = new Set(
- ((remote || []) as any[])
- .map(r => r.addon_id as string)
- .filter((id: string) => !exclude.has(id))
- );
- if (localIds.size !== remoteIds.size) {
- logger.log('[Sync][poll] addons mismatch by count → pull snapshot');
- await this.pullAddonsSnapshot(u.id);
- return;
- }
- for (const id of remoteIds) {
- if (!localIds.has(id)) {
- logger.log('[Sync][poll] addons mismatch by set → pull snapshot');
- await this.pullAddonsSnapshot(u.id);
- break;
- }
- }
- } catch (e) {
- // silent
- }
- }, 21600000); // Increased from 4 hours to 6 hours to reduce background CPU
- };
-
- unsubscribeRealtime = (): void => {
- try {
- logger.log(`[Sync] Realtime unsubscribe (${this.realtimeChannels.length})`);
- for (const ch of this.realtimeChannels) {
- try { ch.unsubscribe?.(); } catch {}
- }
- } finally {
- this.realtimeChannels = [];
- if (this.addonsPollInterval) {
- clearInterval(this.addonsPollInterval);
- this.addonsPollInterval = null;
- }
- if (this.libraryUnsubscribe) {
- try { this.libraryUnsubscribe(); } catch {}
- this.libraryUnsubscribe = null;
- }
- }
- };
-
- async migrateLocalScopeToUser(): Promise {
- const user = await accountService.getCurrentUser();
- if (!user) return;
- const userId = user.id;
- const keys = await AsyncStorage.getAllKeys();
- const migrations: Array> = [];
- const moveKey = async (from: string, to: string) => {
- const val = await AsyncStorage.getItem(from);
- if (val == null) return;
- const exists = await AsyncStorage.getItem(to);
- if (!exists) {
- await AsyncStorage.setItem(to, val);
- } else {
- // Prefer the one with newer lastUpdated if JSON
- try {
- const a = JSON.parse(val);
- const b = JSON.parse(exists);
- const aLU = a?.lastUpdated ?? 0;
- const bLU = b?.lastUpdated ?? 0;
- if (aLU > bLU) await AsyncStorage.setItem(to, val);
- } catch {
- // Keep existing if equal
- }
- }
- await AsyncStorage.removeItem(from);
- };
-
- // Watch progress/content durations/subtitles/app settings
- for (const k of keys) {
- if (k.startsWith('@user:local:@watch_progress:')) {
- const suffix = k.replace('@user:local:@watch_progress:', '');
- migrations.push(moveKey(k, `@user:${userId}:@watch_progress:${suffix}`));
- } else if (k.startsWith('@user:local:@content_duration:')) {
- const suffix = k.replace('@user:local:@content_duration:', '');
- migrations.push(moveKey(k, `@user:${userId}:@content_duration:${suffix}`));
- } else if (k === '@user:local:@subtitle_settings') {
- migrations.push(moveKey(k, `@user:${userId}:@subtitle_settings`));
- } else if (k === 'app_settings') {
- migrations.push(moveKey('app_settings', `@user:${userId}:app_settings`));
- } else if (k === '@user:local:app_settings') {
- migrations.push(moveKey(k, `@user:${userId}:app_settings`));
- } else if (k === '@user:local:stremio-addons' || k === 'stremio-addons') {
- migrations.push(moveKey(k, `@user:${userId}:stremio-addons`));
- } else if (k === '@user:local:stremio-addon-order') {
- migrations.push(moveKey(k, `@user:${userId}:stremio-addon-order`));
- // Do NOT migrate local scraper keys; they are device-local and unscoped
- } else if (k === '@user:local:local-scrapers') {
- // intentionally skip
- } else if (k === '@user:local:scraper-repository-url') {
- // intentionally skip
- } else if (k === '@user:local:stremio-library') {
- migrations.push((async () => {
- const val = (await AsyncStorage.getItem(k)) || '{}';
- await moveKey(k, `@user:${userId}:stremio-library`);
- try {
- const parsed = JSON.parse(val) as Record;
- const count = Array.isArray(parsed) ? parsed.length : Object.keys(parsed || {}).length;
- if (count > 0) await AsyncStorage.setItem(`@user:${userId}:library_initialized`, 'true');
- } catch {}
- })());
- } else if (k === 'stremio-library') {
- migrations.push((async () => {
- const val = (await AsyncStorage.getItem(k)) || '{}';
- await moveKey(k, `@user:${userId}:stremio-library`);
- try {
- const parsed = JSON.parse(val) as Record;
- const count = Array.isArray(parsed) ? parsed.length : Object.keys(parsed || {}).length;
- if (count > 0) await AsyncStorage.setItem(`@user:${userId}:library_initialized`, 'true');
- } catch {}
- })());
- }
- }
- // Migrate legacy theme keys into scoped app_settings
- try {
- const legacyThemeId = await AsyncStorage.getItem('current_theme');
- const legacyCustomThemesJson = await AsyncStorage.getItem('custom_themes');
- const scopedSettingsKey = `@user:${userId}:app_settings`;
- let scopedSettings: any = {};
- try { scopedSettings = JSON.parse((await AsyncStorage.getItem(scopedSettingsKey)) || '{}'); } catch {}
- let changed = false;
- if (legacyThemeId && scopedSettings.themeId !== legacyThemeId) {
- scopedSettings.themeId = legacyThemeId;
- changed = true;
- }
- if (legacyCustomThemesJson) {
- const legacyCustomThemes = JSON.parse(legacyCustomThemesJson);
- if (Array.isArray(legacyCustomThemes)) {
- scopedSettings.customThemes = legacyCustomThemes;
- changed = true;
- }
- }
- if (changed) {
- await AsyncStorage.setItem(scopedSettingsKey, JSON.stringify(scopedSettings));
- }
- } catch {}
- await Promise.all(migrations);
- logger.log(`[Sync] migrateLocalScopeToUser done (moved ~${migrations.length} keys)`);
- }
-
- async fullPush(): Promise {
- logger.log('[Sync] fullPush start');
- await Promise.allSettled([
- this.pushWatchProgress(),
- // Settings push only at app start/sign-in handled by fullPush itself; keep here OK
- this.pushSettings(),
- this.pushAddons(),
- // Excluded: this.pushLocalScrapers(),
- this.pushLibrary(),
- ]);
- logger.log('[Sync] fullPush done');
- }
-
- async fullPull(): Promise {
- logger.log('[Sync] fullPull start');
- const user = await accountService.getCurrentUser();
- if (!user) return;
- const userId = user.id;
- const traktActive = await traktService.isAuthenticated();
-
- await Promise.allSettled([
- (!traktActive ? (async () => {
- logger.log('[Sync] pull watch_progress');
- const { data: wp } = await supabase
- .from('watch_progress')
- .select('*')
- .eq('user_id', userId)
- .is('deleted_at', null);
- if (wp && Array.isArray(wp)) {
- const remoteActiveKeys = new Set();
- for (const row of wp as any[]) {
- // Preserve the most recent timestamp between local and remote to maintain proper continue watching order
- const remoteTimestamp = row.last_updated_ms || Date.now();
- const existingProgress = await storageService.getWatchProgress(
- row.media_id,
- row.media_type,
- (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined
- );
- const localTimestamp = existingProgress?.lastUpdated || 0;
-
- // Use the newer timestamp to maintain proper continue watching order across devices
- const finalTimestamp = Math.max(remoteTimestamp, localTimestamp);
-
- await storageService.setWatchProgress(
- row.media_id,
- row.media_type,
- {
- currentTime: row.current_time_seconds,
- duration: row.duration_seconds,
- lastUpdated: finalTimestamp,
- traktSynced: row.trakt_synced ?? undefined,
- traktLastSynced: row.trakt_last_synced_ms ?? undefined,
- traktProgress: row.trakt_progress_percent ?? undefined,
- },
- // Ensure full episode_id is preserved; treat empty as undefined
- (row.episode_id && row.episode_id.length > 0) ? row.episode_id : undefined,
- { preserveTimestamp: true, forceNotify: true, forceWrite: true }
- );
- remoteActiveKeys.add(`${row.media_type}|${row.media_id}|${row.episode_id || ''}`);
- }
- // Remove any local progress not present on server (server is source of truth)
- try {
- const allLocal = await storageService.getAllWatchProgress();
- for (const [key] of Object.entries(allLocal)) {
- const parts = key.split(':');
- const type = parts[0];
- const id = parts[1];
- const ep = parts[2] || '';
- const k = `${type}|${id}|${ep}`;
- if (!remoteActiveKeys.has(k)) {
- this.suppressPush = true;
- await storageService.removeWatchProgress(id, type, ep || undefined);
- this.suppressPush = false;
- }
- }
- } catch {}
- }
- })() : Promise.resolve()),
- (async () => {
- logger.log('[Sync] pull user_settings');
- const { data: us } = await supabase
- .from('user_settings')
- .select('*')
- .eq('user_id', userId)
- .single();
- if (us) {
- // Merge remote settings with existing local settings, preferring remote values
- // but preserving any local-only keys (e.g., newly added client-side settings
- // not yet present on the server). This avoids losing local preferences on restart.
- try {
- const localScopedJson = (await AsyncStorage.getItem(`@user:${userId}:app_settings`)) || '{}';
- const localLegacyJson = (await AsyncStorage.getItem('app_settings')) || '{}';
- // Prefer scoped local if available; fall back to legacy
- let localSettings: Record = {};
- try { localSettings = JSON.parse(localScopedJson); } catch {}
- if (!localSettings || Object.keys(localSettings).length === 0) {
- try { localSettings = JSON.parse(localLegacyJson); } catch { localSettings = {}; }
- }
-
- const remoteRaw: Record = (us.app_settings || {}) as Record;
- // Exclude episodeLayoutStyle from remote to keep it local-only
- const { episodeLayoutStyle: _remoteEpisodeLayoutStyle, ...remoteSettingsSansLocalOnly } = remoteRaw || {};
- // Merge: start from local, override with remote (sans excluded keys)
- const mergedSettings = { ...(localSettings || {}), ...(remoteSettingsSansLocalOnly || {}) };
-
- await AsyncStorage.setItem(`@user:${userId}:app_settings`, JSON.stringify(mergedSettings));
- await AsyncStorage.setItem('app_settings', JSON.stringify(mergedSettings));
-
- // Sync continue watching removed items (stored in app_settings)
- if (remoteSettingsSansLocalOnly?.continue_watching_removed) {
- await AsyncStorage.setItem(`@user:${userId}:@continue_watching_removed`, JSON.stringify(remoteSettingsSansLocalOnly.continue_watching_removed));
- }
-
- await storageService.saveSubtitleSettings(us.subtitle_settings || {});
- // Notify listeners that settings changed due to sync
- try { settingsEmitter.emit(); } catch {}
- } catch (e) {
- // Fallback to writing remote settings as-is if merge fails
- const remoteRaw: Record = (us.app_settings || {}) as Record;
- const { episodeLayoutStyle: _remoteEpisodeLayoutStyle, ...remoteSettingsSansLocalOnly } = remoteRaw || {};
- await AsyncStorage.setItem(`@user:${userId}:app_settings`, JSON.stringify(remoteSettingsSansLocalOnly));
- await AsyncStorage.setItem('app_settings', JSON.stringify(remoteSettingsSansLocalOnly));
-
- // Sync continue watching removed items in fallback (stored in app_settings)
- if (remoteSettingsSansLocalOnly?.continue_watching_removed) {
- await AsyncStorage.setItem(`@user:${userId}:@continue_watching_removed`, JSON.stringify(remoteSettingsSansLocalOnly.continue_watching_removed));
- }
-
- await storageService.saveSubtitleSettings(us.subtitle_settings || {});
- try { settingsEmitter.emit(); } catch {}
- }
- }
- })(),
- this.smartPullAddons(userId), // Use smart pull instead of destructive pull
- this.pullLibrary(userId),
- ]);
- logger.log('[Sync] fullPull done');
- }
-
- private async pullLibrary(userId: string): Promise {
- try {
- logger.log('[Sync] pull user_library');
- const { data, error } = await supabase
- .from('user_library')
- .select('media_type, media_id, title, poster_url, year, deleted_at, updated_at')
- .eq('user_id', userId);
- if (error) {
- if (__DEV__) console.warn('[SyncService] pull library error', error);
- return;
- }
- const obj: Record = {};
- for (const row of (data || []) as any[]) {
- if (row.deleted_at) continue;
- const key = `${row.media_type}:${row.media_id}`;
- obj[key] = {
- id: row.media_id,
- type: row.media_type,
- name: row.title || row.media_id,
- poster: row.poster_url || '',
- year: row.year || undefined,
- inLibrary: true,
- };
- }
- await AsyncStorage.setItem(`@user:${userId}:stremio-library`, JSON.stringify(obj));
- await AsyncStorage.setItem('stremio-library', JSON.stringify(obj));
- logger.log(`[Sync] pull user_library wrote items=${Object.keys(obj).length}`);
- } catch (e) {
- if (__DEV__) console.warn('[SyncService] pullLibrary exception', e);
- }
- }
-
- private async pushLibrary(): Promise {
- const user = await accountService.getCurrentUser();
- if (!user) return;
- try {
- const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
- const json =
- (await AsyncStorage.getItem(`@user:${scope}:stremio-library`)) ||
- (await AsyncStorage.getItem('stremio-library')) || '{}';
- const itemsObj = JSON.parse(json) as Record;
- const entries = Object.values(itemsObj) as any[];
- logger.log(`[Sync] push user_library entries=${entries.length}`);
- const initialized = (await AsyncStorage.getItem(`@user:${user.id}:library_initialized`)) === 'true';
- // If not initialized and local entries are 0, attempt to import from server first
- if (!initialized && entries.length === 0) {
- logger.log('[Sync] user_library not initialized and local empty → pulling before deletions');
- await this.pullLibrary(user.id);
- const post = (await AsyncStorage.getItem(`@user:${user.id}:stremio-library`)) || '{}';
- const postObj = JSON.parse(post) as Record;
- const postEntries = Object.values(postObj) as any[];
- if (postEntries.length > 0) {
- await AsyncStorage.setItem(`@user:${user.id}:library_initialized`, 'true');
- }
- }
- // Upsert rows
- if (entries.length > 0) {
- const rows = entries.map((it) => ({
- user_id: user.id,
- media_type: it.type === 'movie' ? 'movie' : 'series',
- media_id: it.id,
- title: it.name || it.title || it.id,
- poster_url: it.poster || it.poster_url || null,
- year: normalizeYear(it.year),
- updated_at: new Date().toISOString(),
- }));
- const { error: upErr } = await supabase
- .from('user_library')
- .upsert(rows, { onConflict: 'user_id,media_type,media_id' });
- if (upErr && __DEV__) console.warn('[SyncService] push library upsert error', upErr);
- else await AsyncStorage.setItem(`@user:${user.id}:library_initialized`, 'true');
- }
- // No computed deletions; removals happen only via explicit user action (soft delete)
- } catch (e) {
- if (__DEV__) console.warn('[SyncService] pushLibrary exception', e);
- }
- }
-
- private async pushLibraryAdd(item: StreamingContent): Promise {
- const user = await accountService.getCurrentUser();
- if (!user) return;
- try {
- const row = {
- user_id: user.id,
- media_type: item.type === 'movie' ? 'movie' : 'series',
- media_id: item.id,
- title: (item as any).name || (item as any).title || item.id,
- poster_url: (item as any).poster || null,
- year: normalizeYear((item as any).year),
- deleted_at: null as any,
- updated_at: new Date().toISOString(),
- };
- const { error } = await supabase.from('user_library').upsert(row, { onConflict: 'user_id,media_type,media_id' });
- if (error && __DEV__) console.warn('[SyncService] pushLibraryAdd error', error);
- } catch (e) {
- if (__DEV__) console.warn('[SyncService] pushLibraryAdd exception', e);
- }
- }
-
- private async pushLibraryRemove(type: string, id: string): Promise {
- const user = await accountService.getCurrentUser();
- if (!user) return;
- try {
- const { error } = await supabase
- .from('user_library')
- .update({ deleted_at: new Date().toISOString(), updated_at: new Date().toISOString() })
- .eq('user_id', user.id)
- .eq('media_type', type === 'movie' ? 'movie' : 'series')
- .eq('media_id', id);
- if (error && __DEV__) console.warn('[SyncService] pushLibraryRemove error', error);
- } catch (e) {
- if (__DEV__) console.warn('[SyncService] pushLibraryRemove exception', e);
- }
- }
-
- private async pullAddonsSnapshot(userId: string): Promise {
- logger.log('[Sync] pull installed_addons');
- const { data: addons, error: addonsErr } = await supabase
- .from('installed_addons')
- .select('*')
- .eq('user_id', userId)
- .order('position', { ascending: true });
- if (addonsErr) {
- if (__DEV__) console.warn('[SyncService] pull addons error', addonsErr);
- return;
- }
- if (!(addons && Array.isArray(addons))) return;
-
- // Start from currently installed (to preserve pre-installed like Cinemeta/OpenSubtitles)
- const map = new Map();
-
- for (const a of addons as any[]) {
- try {
- // Skip server addon if user explicitly removed it locally (tombstone)
- try {
- const removed = await stremioService.hasUserRemovedAddon(a.addon_id);
- if (removed && a.addon_id !== 'com.linvo.cinemeta') {
- continue;
- }
- } catch {}
- let manifest = a.manifest_data;
- if (!manifest) {
- const urlToUse = a.original_url || a.url;
- if (urlToUse) {
- manifest = await stremioService.getManifest(urlToUse);
- }
- }
- if (!manifest) {
- manifest = {
- id: a.addon_id,
- name: a.name || a.addon_id,
- version: a.version || '1.0.0',
- description: a.description || '',
- url: a.url || a.original_url || '',
- originalUrl: a.original_url || a.url || '',
- catalogs: [],
- resources: [],
- types: [],
- };
- }
- manifest.id = a.addon_id;
- map.set(a.addon_id, manifest);
- } catch (e) {
- if (__DEV__) console.warn('[SyncService] failed to fetch manifest for', a.addon_id, e);
- }
- }
-
- // Always include preinstalled regardless of server
- try { map.set('com.linvo.cinemeta', await stremioService.getManifest('https://v3-cinemeta.strem.io/manifest.json')); } catch {}
-
- // Only include OpenSubtitles if user hasn't explicitly removed it
- const hasUserRemovedOpenSubtitles = await stremioService.hasUserRemovedAddon('org.stremio.opensubtitlesv3');
- if (!hasUserRemovedOpenSubtitles) {
- try { map.set('org.stremio.opensubtitlesv3', await stremioService.getManifest('https://opensubtitles-v3.strem.io/manifest.json')); } catch {}
- }
-
- (stremioService as any).installedAddons = map;
- let order = (addons as any[]).map(a => a.addon_id);
- const ensureFront = (arr: string[], id: string) => {
- const idx = arr.indexOf(id);
- if (idx === -1) arr.unshift(id);
- else if (idx > 0) { arr.splice(idx, 1); arr.unshift(id); }
- };
- ensureFront(order, 'com.linvo.cinemeta');
-
- // Only ensure OpenSubtitles is in order if user hasn't removed it
- if (!hasUserRemovedOpenSubtitles) {
- ensureFront(order, 'org.stremio.opensubtitlesv3');
- }
- // Prefer local order if it exists; otherwise use remote
- try {
- const userScope = `@user:${userId}:stremio-addon-order`;
- const [localScopedOrder, localLegacyOrder, localGuestOrder] = await Promise.all([
- AsyncStorage.getItem(userScope),
- AsyncStorage.getItem('stremio-addon-order'),
- AsyncStorage.getItem('@user:local:stremio-addon-order'),
- ]);
- const localOrderRaw = localScopedOrder || localLegacyOrder || localGuestOrder;
- if (localOrderRaw) {
- const localOrder = JSON.parse(localOrderRaw) as string[];
- // Filter to only installed ids
- const localFiltered = localOrder.filter(id => map.has(id));
- if (localFiltered.length > 0) {
- order = localFiltered;
- }
- }
- } catch {}
-
- (stremioService as any).addonOrder = order;
- await (stremioService as any).saveInstalledAddons();
- await (stremioService as any).saveAddonOrder();
- // Mark addons initialized for this user to prevent destructive merges on first push
- try { await AsyncStorage.setItem(`@user:${userId}:addons_initialized`, 'true'); } catch {}
- // Push merged order to server to preserve across devices
- try {
- const rows = order.map((addonId: string, idx: number) => ({
- user_id: userId,
- addon_id: addonId,
- position: idx,
- }));
- const { error } = await supabase
- .from('installed_addons')
- .upsert(rows, { onConflict: 'user_id,addon_id' });
- if (error) logger.warn('[SyncService] push merged addon order error', error);
- } catch (e) {
- logger.warn('[SyncService] push merged addon order exception', e);
- }
- }
-
- async pushWatchProgress(): Promise {
- const user = await accountService.getCurrentUser();
- if (!user) return;
- // When Trakt is authenticated, disable account push for continue watching
- try {
- if (await traktService.isAuthenticated()) {
- logger.log('[Sync] Trakt active → skipping push watch_progress');
- return;
- }
- } catch {}
- const userId = user.id;
- const unsynced = await storageService.getUnsyncedProgress();
- logger.log(`[Sync] push watch_progress rows=${unsynced.length}`);
- const rows: any[] = unsynced.map(({ id, type, episodeId, progress }) => ({
- user_id: userId,
- media_type: type,
- media_id: id,
- episode_id: episodeId || '',
- current_time_seconds: Math.floor(progress.currentTime || 0),
- duration_seconds: Math.floor(progress.duration || 0),
- last_updated_ms: progress.lastUpdated || Date.now(),
- trakt_synced: progress.traktSynced ?? undefined,
- trakt_last_synced_ms: progress.traktLastSynced ?? undefined,
- trakt_progress_percent: progress.traktProgress ?? undefined,
- deleted_at: null,
- updated_at: new Date().toISOString(),
- }));
- if (rows.length > 0) {
- // Prevent resurrecting remotely-deleted rows when server has newer update
- try {
- const keys = rows.map(r => ({ media_type: r.media_type, media_id: r.media_id, episode_id: r.episode_id }));
- const { data: remote } = await supabase
- .from('watch_progress')
- .select('media_type,media_id,episode_id,deleted_at,updated_at')
- .eq('user_id', userId)
- .in('media_type', keys.map(k => k.media_type))
- .in('media_id', keys.map(k => k.media_id))
- .in('episode_id', keys.map(k => k.episode_id));
- const shouldSkip = new Set();
- if (remote) {
- for (const r of remote as any[]) {
- const key = `${r.media_type}|${r.media_id}|${r.episode_id || ''}`;
- if (r.deleted_at && r.updated_at) {
- const remoteUpdatedMs = new Date(r.updated_at as string).getTime();
- // Find matching local row
- const local = rows.find(x => x.media_type === r.media_type && x.media_id === r.media_id && x.episode_id === (r.episode_id || ''));
- const localUpdatedMs = local?.last_updated_ms ?? 0;
- if (remoteUpdatedMs >= localUpdatedMs) {
- shouldSkip.add(key);
- // also write a tombstone locally
- try { await storageService.addWatchProgressTombstone(r.media_id, r.media_type, r.episode_id || undefined, remoteUpdatedMs); } catch {}
- }
- }
- }
- }
- if (shouldSkip.size > 0) {
- logger.log(`[Sync] push watch_progress skipping resurrect count=${shouldSkip.size}`);
- }
- // Filter rows to upsert
- const filteredRows = rows.filter(r => !shouldSkip.has(`${r.media_type}|${r.media_id}|${r.episode_id}`));
- if (filteredRows.length > 0) {
- const { error } = await supabase
- .from('watch_progress')
- .upsert(filteredRows, { onConflict: 'user_id,media_type,media_id,episode_id' });
- if (error && __DEV__) console.warn('[SyncService] push watch_progress error', error);
- else logger.log('[Sync] push watch_progress upsert ok');
- }
- } catch (e) {
- // Fallback to normal upsert if pre-check fails
- const { error } = await supabase
- .from('watch_progress')
- .upsert(rows, { onConflict: 'user_id,media_type,media_id,episode_id' });
- if (error && __DEV__) console.warn('[SyncService] push watch_progress error', error);
- else logger.log('[Sync] push watch_progress upsert ok');
- }
- }
-
- // Deletions occur only on explicit remove; no bulk deletions here
- }
-
- private async softDeleteWatchProgress(type: string, id: string, episodeId?: string): Promise {
- const user = await accountService.getCurrentUser();
- if (!user) return;
- // When Trakt is authenticated, do not propagate deletes to account server for watch progress
- try {
- if (await traktService.isAuthenticated()) {
- logger.log('[Sync] Trakt active → skipping softDelete watch_progress');
- return;
- }
- } catch {}
- try {
- const { error } = await supabase
- .from('watch_progress')
- .update({ deleted_at: new Date().toISOString(), updated_at: new Date().toISOString() })
- .eq('user_id', user.id)
- .eq('media_type', type)
- .eq('media_id', id)
- .eq('episode_id', episodeId || '');
- if (error && __DEV__) console.warn('[SyncService] softDeleteWatchProgress error', error);
- } catch (e) {
- if (__DEV__) console.warn('[SyncService] softDeleteWatchProgress exception', e);
- }
- }
-
- async pushSettings(): Promise {
- const user = await accountService.getCurrentUser();
- if (!user) return;
- const userId = user.id;
- logger.log('[Sync] push user_settings start');
- const scope = (await AsyncStorage.getItem('@user:current')) || 'local';
- const appSettingsJson =
- (await AsyncStorage.getItem(`@user:${scope}:app_settings`)) ||
- (await AsyncStorage.getItem('app_settings')) ||
- '{}';
- const parsed = JSON.parse(appSettingsJson) as Record;
- // Exclude local-only settings from push
- const { episodeLayoutStyle: _localEpisodeLayoutStyle, ...appSettings } = parsed || {};
- const subtitleSettings = (await storageService.getSubtitleSettings()) || {};
- const continueWatchingRemoved = await storageService.getContinueWatchingRemoved();
-
- // Include continue watching removed items in app_settings
- const appSettingsWithRemoved = {
- ...appSettings,
- continue_watching_removed: continueWatchingRemoved
- };
-
- const { error } = await supabase.from('user_settings').upsert({
- user_id: userId,
- app_settings: appSettingsWithRemoved,
- subtitle_settings: subtitleSettings,
- });
- if (error && __DEV__) console.warn('[SyncService] push settings error', error);
- else logger.log('[Sync] push user_settings ok');
- }
-
- async pushAddons(): Promise {
- const user = await accountService.getCurrentUser();
- if (!user) return;
- const userId = user.id;
- let addons = await stremioService.getInstalledAddonsAsync();
- logger.log(`[Sync] push installed_addons count=${addons.length}`);
- let order = (stremioService as any).addonOrder as string[];
-
- // Safety: Check if this device has ever synced addons for this user
- // Only pull if this is truly a first-time sync AND remote has significantly more addons
- try {
- const deviceInitialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true';
- const { data: remoteBefore } = await supabase
- .from('installed_addons')
- .select('addon_id')
- .eq('user_id', userId);
- const remoteCount = (remoteBefore || []).length;
-
- // Only pull if:
- // 1. This device hasn't initialized addons for this user
- // 2. Remote has significantly more addons (not just pre-installed ones)
- // 3. Local has only pre-installed addons (2 or fewer)
- const hasOnlyPreInstalled = addons.length <= 2 &&
- addons.every(a => ['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3'].includes(a.id));
-
- if (!deviceInitialized && remoteCount > 2 && hasOnlyPreInstalled) {
- logger.log('[Sync] Device first-time sync with only pre-installed addons → pulling before push');
- await this.pullAddonsSnapshot(userId);
- // refresh local state after pull
- addons = await stremioService.getInstalledAddonsAsync();
- order = (stremioService as any).addonOrder as string[];
- } else if (!deviceInitialized && remoteCount > addons.length) {
- logger.log('[Sync] Device first-time sync but local has custom addons → merging instead of pulling');
- // Don't pull - merge the addons instead
- await this.mergeAddonsFromServer(userId);
- addons = await stremioService.getInstalledAddonsAsync();
- order = (stremioService as any).addonOrder as string[];
- }
- } catch {}
-
- const removedListJson = (await AsyncStorage.getItem('user_removed_addons')) || '[]';
- let removedList: string[] = [];
- try { removedList = JSON.parse(removedListJson); } catch { removedList = []; }
-
- const rows = addons.map((a: any) => ({
- user_id: userId,
- addon_id: a.id,
- name: a.name,
- url: a.url,
- original_url: a.originalUrl,
- version: a.version,
- description: a.description,
- position: Math.max(0, order.indexOf(a.id)),
- manifest_data: a,
- }));
- // Delete remote addons that no longer exist locally (excluding pre-installed to be safe)
- // Enhanced safety: only delete if device has been initialized and user explicitly removed addons
- try {
- const { data: remote, error: rErr } = await supabase
- .from('installed_addons')
- .select('addon_id')
- .eq('user_id', userId);
- if (!rErr && remote) {
- const deviceInitialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true';
-
- // Only perform deletions if:
- // 1. Device has been initialized (not first-time sync)
- // 2. Local addons are not just pre-installed ones
- const hasOnlyPreInstalled = addons.length <= 2 &&
- addons.every(a => ['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3'].includes(a.id));
-
- if (!deviceInitialized || hasOnlyPreInstalled) {
- logger.log('[Sync] skipping deletions during first-time sync or when only pre-installed addons present');
- } else {
- const localIds = new Set(addons.map((a: any) => a.id));
- const toDeletePromises = (remote as any[])
- .map(r => r.addon_id as string)
- .map(async id => {
- if (localIds.has(id)) return null; // Don't delete if still installed locally
- // If user removed Cinemeta locally, allow server deletion as well
- // If user explicitly removed this addon locally, prefer local removal and delete remotely
- if (removedList.includes(id)) return id;
- return id; // Delete other addons that are no longer installed locally
- });
-
- const toDeleteResults = await Promise.all(toDeletePromises);
- const toDelete = toDeleteResults.filter(id => id !== null);
- logger.log(`[Sync] push installed_addons deletions=${toDelete.length}`);
- if (toDelete.length > 0) {
- const del = await supabase
- .from('installed_addons')
- .delete()
- .eq('user_id', userId)
- .in('addon_id', toDelete);
- if (del.error && __DEV__) console.warn('[SyncService] delete addons error', del.error);
- }
- }
- }
- } catch (e) {
- if (__DEV__) console.warn('[SyncService] deletion sync for addons failed', e);
- }
- const { error } = await supabase.from('installed_addons').upsert(rows, { onConflict: 'user_id,addon_id' });
- if (error && __DEV__) console.warn('[SyncService] push addons error', error);
- }
-
- // Excluded: pushLocalScrapers (local scrapers are device-local only)
-
- private async smartPullAddons(userId: string): Promise {
- logger.log('[Sync] smartPullAddons: intelligent addon synchronization');
- try {
- // Check if this device has been initialized for this user
- const deviceInitialized = (await AsyncStorage.getItem(`@user:${userId}:addons_initialized`)) === 'true';
-
- // Get current local addons
- const localAddons = await stremioService.getInstalledAddonsAsync();
- const localAddonIds = new Set(localAddons.map(a => a.id));
-
- // Get remote addons
- const { data: remoteAddons } = await supabase
- .from('installed_addons')
- .select('*')
- .eq('user_id', userId)
- .order('position', { ascending: true });
-
- if (!remoteAddons || remoteAddons.length === 0) {
- logger.log('[Sync] smartPullAddons: no remote addons found');
- return;
- }
-
- const remoteAddonIds = new Set(remoteAddons.map(a => a.addon_id));
-
- // Determine sync strategy based on context
- const hasOnlyPreInstalled = localAddons.length <= 2 &&
- localAddons.every(a => ['com.linvo.cinemeta', 'org.stremio.opensubtitlesv3'].includes(a.id));
-
- if (!deviceInitialized && hasOnlyPreInstalled && remoteAddons.length > 2) {
- // First-time sync with only pre-installed addons - safe to pull
- logger.log('[Sync] smartPullAddons: first-time sync with only pre-installed → pulling');
- await this.pullAddonsSnapshot(userId);
- } else if (!deviceInitialized && localAddons.length > 2) {
- // First-time sync but user has custom addons - merge instead
- logger.log('[Sync] smartPullAddons: first-time sync with custom addons → merging');
- await this.mergeAddonsFromServer(userId);
- } else if (deviceInitialized) {
- // Device already initialized - only merge missing addons
- logger.log('[Sync] smartPullAddons: device initialized → merging missing addons only');
- await this.mergeMissingAddonsOnly(userId, localAddonIds, remoteAddons);
- } else {
- // Default case - merge
- logger.log('[Sync] smartPullAddons: default case → merging');
- await this.mergeAddonsFromServer(userId);
- }
-
- // Mark device as initialized after successful sync
- if (!deviceInitialized) {
- await AsyncStorage.setItem(`@user:${userId}:addons_initialized`, 'true');
- logger.log('[Sync] smartPullAddons: marked device as initialized');
- }
-
- } catch (e) {
- logger.error('[Sync] smartPullAddons failed:', e);
- }
- }
-
- private async mergeMissingAddonsOnly(userId: string, localAddonIds: Set, remoteAddons: any[]): Promise {
- logger.log('[Sync] mergeMissingAddonsOnly: adding only missing addons');
- try {
- const addonsToInstall: any[] = [];
-
- for (const remoteAddon of remoteAddons) {
- if (!localAddonIds.has(remoteAddon.addon_id)) {
- // Honor local tombstone: skip addons user explicitly removed
- try {
- const removed = await stremioService.hasUserRemovedAddon(remoteAddon.addon_id);
- if (removed && remoteAddon.addon_id !== 'com.linvo.cinemeta') {
- continue;
- }
- } catch {}
- try {
- let manifest = remoteAddon.manifest_data;
- if (!manifest && remoteAddon.original_url) {
- manifest = await stremioService.getManifest(remoteAddon.original_url);
- }
- if (manifest) {
- addonsToInstall.push(manifest);
- }
- } catch (e) {
- logger.warn('[Sync] Failed to fetch manifest for missing addon:', remoteAddon.addon_id);
- }
- }
- }
-
- // Install missing addons locally
- for (const manifest of addonsToInstall) {
- try {
- await stremioService.installAddon(manifest.originalUrl || manifest.url);
- logger.log('[Sync] Installed missing addon:', manifest.id);
- } catch (e) {
- logger.warn('[Sync] Failed to install missing addon:', manifest.id);
- }
- }
-
- logger.log(`[Sync] mergeMissingAddonsOnly completed: ${addonsToInstall.length} addons installed`);
- } catch (e) {
- logger.error('[Sync] mergeMissingAddonsOnly failed:', e);
- }
- }
-
- private async mergeAddonsFromServer(userId: string): Promise {
- logger.log('[Sync] mergeAddonsFromServer: merging server addons with local addons');
- try {
- const { data: remoteAddons } = await supabase
- .from('installed_addons')
- .select('*')
- .eq('user_id', userId)
- .order('position', { ascending: true });
-
- if (!remoteAddons || remoteAddons.length === 0) return;
-
- // Get current local addons
- const localAddons = await stremioService.getInstalledAddonsAsync();
- const localAddonIds = new Set(localAddons.map(a => a.id));
-
- // Merge remote addons that aren't already local
- const addonsToInstall: any[] = [];
- for (const remoteAddon of remoteAddons as any[]) {
- if (!localAddonIds.has(remoteAddon.addon_id)) {
- // Honor local tombstone: skip addons user explicitly removed
- try {
- const removed = await stremioService.hasUserRemovedAddon(remoteAddon.addon_id);
- if (removed && remoteAddon.addon_id !== 'com.linvo.cinemeta') {
- continue;
- }
- } catch {}
- try {
- let manifest = remoteAddon.manifest_data;
- if (!manifest && remoteAddon.original_url) {
- manifest = await stremioService.getManifest(remoteAddon.original_url);
- }
- if (manifest) {
- addonsToInstall.push(manifest);
- }
- } catch (e) {
- logger.warn('[Sync] Failed to fetch manifest for remote addon:', remoteAddon.addon_id);
- }
- }
- }
-
- // Install missing addons locally
- for (const manifest of addonsToInstall) {
- try {
- await stremioService.installAddon(manifest.originalUrl || manifest.url);
- logger.log('[Sync] Merged addon from server:', manifest.id);
- } catch (e) {
- logger.warn('[Sync] Failed to install merged addon:', manifest.id);
- }
- }
-
- logger.log(`[Sync] mergeAddonsFromServer completed: ${addonsToInstall.length} addons merged`);
- } catch (e) {
- logger.error('[Sync] mergeAddonsFromServer failed:', e);
- }
- }
-}
-
-export const syncService = SyncService.getInstance();
-export default syncService;
-
-// Small helper to batch delete operations
-function chunkArray(arr: T[], size: number): T[][] {
- const res: T[][] = [];
- for (let i = 0; i < arr.length; i += size) res.push(arr.slice(i, i + size));
- return res;
-}
-
-// Normalize year values to integer or null
-function normalizeYear(value: any): number | null {
- if (value == null) return null;
- if (typeof value === 'number' && Number.isInteger(value)) return value;
- if (typeof value === 'string') {
- // Extract first 4 consecutive digits
- const m = value.match(/\d{4}/);
- if (m) {
- const y = parseInt(m[0], 10);
- if (y >= 1900 && y <= 2100) return y;
- return y;
- }
- }
- return null;
-}
-
diff --git a/src/services/stremioService.ts b/src/services/stremioService.ts
index c1a3947..225d428 100644
--- a/src/services/stremioService.ts
+++ b/src/services/stremioService.ts
@@ -573,7 +573,6 @@ class StremioService {
await this.saveInstalledAddons();
await this.saveAddonOrder();
- try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that an addon was added
addonEmitter.emit(ADDON_EVENTS.ADDON_ADDED, manifest.id);
} else {
@@ -596,7 +595,6 @@ class StremioService {
// Persist removals before app possibly exits
await this.saveInstalledAddons();
await this.saveAddonOrder();
- try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that an addon was removed
addonEmitter.emit(ADDON_EVENTS.ADDON_REMOVED, id);
}
@@ -1634,11 +1632,9 @@ class StremioService {
const index = this.addonOrder.indexOf(id);
if (index > 0) {
// Swap with the previous item
- [this.addonOrder[index - 1], this.addonOrder[index]] =
+ [this.addonOrder[index - 1], this.addonOrder[index]] =
[this.addonOrder[index], this.addonOrder[index - 1]];
this.saveAddonOrder();
- // Immediately push to server to avoid resets on restart
- try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that the order has changed
addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
return true;
@@ -1650,11 +1646,9 @@ class StremioService {
const index = this.addonOrder.indexOf(id);
if (index >= 0 && index < this.addonOrder.length - 1) {
// Swap with the next item
- [this.addonOrder[index], this.addonOrder[index + 1]] =
+ [this.addonOrder[index], this.addonOrder[index + 1]] =
[this.addonOrder[index + 1], this.addonOrder[index]];
this.saveAddonOrder();
- // Immediately push to server to avoid resets on restart
- try { (require('./SyncService').syncService as any).pushAddons?.(); } catch {}
// Emit an event that the order has changed
addonEmitter.emit(ADDON_EVENTS.ORDER_CHANGED);
return true;
diff --git a/src/services/supabaseClient.ts b/src/services/supabaseClient.ts
deleted file mode 100644
index bf3949b..0000000
--- a/src/services/supabaseClient.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import 'react-native-url-polyfill/auto';
-import 'react-native-get-random-values';
-import { createClient } from '@supabase/supabase-js';
-import AsyncStorage from '@react-native-async-storage/async-storage';
-
-
-const SUPABASE_URL = process.env.EXPO_PUBLIC_SUPABASE_URL;
-const SUPABASE_ANON_KEY = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
-
-if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
- throw new Error('Missing Supabase environment variables. Please check your .env file.');
-}
-
-export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
- auth: {
- persistSession: true,
- storage: AsyncStorage as unknown as Storage,
- autoRefreshToken: true,
- detectSessionInUrl: false,
- },
-});
-
-export default supabase;
-