mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
feat: add Fastlane Google Play upload and fix onboarding re-renders
- Add Fastlane setup for uploading AAB to Google Play - Chain upload into release:android npm script - Fix onboarding re-renders from authStore subscriptions - Reduce unnecessary authStore state updates in fetchUser - Bump Android versionCode to 45
This commit is contained in:
1 parent
de0d667f4f
commit
3afb091e90
8 files changed
+118
-54
No files matched your search
@@ -8,4 +8,8 @@ expo-env.d.ts
|
||||
*.ipa
|
||||
*.apk
|
||||
*.jks
|
||||
*.keystore
|
||||
*.keystore
|
||||
# Fastlane
|
||||
fastlane-key.json
|
||||
fastlane/report.xml
|
||||
fastlane/README.md
|
||||
@@ -91,7 +91,7 @@ android {
|
||||
applicationId 'com.sauci.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 44
|
||||
versionCode 45
|
||||
versionName "1.0.5"
|
||||
}
|
||||
signingConfigs {
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
"backgroundColor": "#1a1a2e"
|
||||
},
|
||||
"package": "com.sauci.app",
|
||||
"versionCode": 44,
|
||||
"versionCode": 45,
|
||||
"googleServicesFile": "./google-services.json",
|
||||
"permissions": [
|
||||
"RECEIVE_BOOT_COMPLETED",
|
||||
|
||||
@@ -64,12 +64,15 @@ type Stage = 'avatar' | 'name' | 'gender' | 'purpose' | 'content' | 'notificatio
|
||||
|
||||
export default function OnboardingScreen() {
|
||||
const router = useRouter();
|
||||
const { user, fetchUser } = useAuthStore();
|
||||
// Only subscribe to stable values to prevent re-renders from background fetchUser calls
|
||||
const userId = useAuthStore((s) => s.user?.id);
|
||||
const fetchUser = useAuthStore((s) => s.fetchUser);
|
||||
const initialUser = useRef(useAuthStore.getState().user);
|
||||
const [stage, setStage] = useState<Stage>('avatar');
|
||||
const [name, setName] = useState(user?.name || '');
|
||||
const [gender, setGender] = useState<Gender | null>(user?.gender || null);
|
||||
const [name, setName] = useState(initialUser.current?.name || '');
|
||||
const [gender, setGender] = useState<Gender | null>(initialUser.current?.gender || null);
|
||||
const [usageReason, setUsageReason] = useState<UsageReason | null>(null);
|
||||
const [hideNsfw, setHideNsfw] = useState(user?.hide_nsfw ?? false);
|
||||
const [hideNsfw, setHideNsfw] = useState(initialUser.current?.hide_nsfw ?? false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showPaywall, setShowPaywall] = useState(false);
|
||||
@@ -82,7 +85,7 @@ export default function OnboardingScreen() {
|
||||
showPicker: handleAvatarPress,
|
||||
uploadAvatar,
|
||||
} = useAvatarPicker({
|
||||
userId: user?.id,
|
||||
userId,
|
||||
onSelect: () => setError(null),
|
||||
});
|
||||
|
||||
@@ -147,8 +150,8 @@ export default function OnboardingScreen() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const token = await registerForPushNotificationsAsync();
|
||||
if (token && user?.id) {
|
||||
await savePushToken(user.id, token);
|
||||
if (token && userId) {
|
||||
await savePushToken(userId, token);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to enable notifications:', err);
|
||||
@@ -159,8 +162,8 @@ export default function OnboardingScreen() {
|
||||
};
|
||||
|
||||
const handleComplete = async () => {
|
||||
console.log('[Onboarding] handleComplete called, user:', user?.id, 'name:', name);
|
||||
if (!user?.id) {
|
||||
console.log('[Onboarding] handleComplete called, user:', userId, 'name:', name);
|
||||
if (!userId) {
|
||||
console.error('[Onboarding] No user ID available');
|
||||
setError('Not logged in. Please restart the app.');
|
||||
return;
|
||||
@@ -204,7 +207,7 @@ export default function OnboardingScreen() {
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update(updateData)
|
||||
.eq('id', user.id);
|
||||
.eq('id', userId);
|
||||
|
||||
if (updateError) {
|
||||
console.error('[Onboarding] Profile update error:', updateError);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
json_key_file(ENV["GOOGLE_PLAY_KEY_FILE"] || File.expand_path("../fastlane-key.json", __dir__))
|
||||
package_name("com.sauci.app")
|
||||
@@ -0,0 +1,48 @@
|
||||
default_platform(:android)
|
||||
|
||||
platform :android do
|
||||
desc "Upload AAB to Google Play internal testing track"
|
||||
lane :upload do |options|
|
||||
track = options[:track] || "internal"
|
||||
|
||||
# Find the AAB file - check project root for EAS builds or gradle output
|
||||
project_root = File.expand_path("..", __dir__)
|
||||
aab_path = options[:aab]
|
||||
|
||||
unless aab_path
|
||||
# Check for EAS build AABs in project root, pick the newest
|
||||
eas_builds = Dir.glob(File.join(project_root, "build-*.aab")).sort_by { |f| File.mtime(f) }
|
||||
gradle_builds = Dir.glob(File.join(project_root, "android/app/build/outputs/bundle/release/*.aab"))
|
||||
aab_path = eas_builds.last || gradle_builds.first
|
||||
end
|
||||
|
||||
# Resolve relative paths from project root
|
||||
aab_path = File.expand_path(aab_path, project_root) if aab_path && !aab_path.start_with?("/")
|
||||
|
||||
UI.user_error!("No AAB file found. Build first or specify with aab: parameter") unless aab_path && File.exist?(aab_path)
|
||||
|
||||
UI.message("Uploading #{aab_path} to #{track} track...")
|
||||
|
||||
upload_to_play_store(
|
||||
track: track,
|
||||
aab: aab_path,
|
||||
skip_upload_metadata: true,
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true,
|
||||
skip_upload_changelogs: false,
|
||||
release_status: "draft",
|
||||
)
|
||||
|
||||
UI.success("Successfully uploaded to #{track} track!")
|
||||
end
|
||||
|
||||
desc "Upload AAB to production track"
|
||||
lane :upload_production do |options|
|
||||
upload(options.merge(track: "production"))
|
||||
end
|
||||
|
||||
desc "Upload AAB to beta track"
|
||||
lane :upload_beta do |options|
|
||||
upload(options.merge(track: "beta"))
|
||||
end
|
||||
end
|
||||
@@ -15,7 +15,7 @@
|
||||
"test:ci": "jest --runInBand --ci",
|
||||
"release:ios": "dotenv -e .env -- npx expo prebuild --platform ios && dotenv -e .env -- eas build --platform ios --profile production --local",
|
||||
"release:ios:preview": "dotenv -e .env -- npx expo prebuild --platform ios && dotenv -e .env -- eas build --platform ios --profile preview --local",
|
||||
"release:android": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile production --local",
|
||||
"release:android": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile production --local && bundle exec fastlane android upload",
|
||||
"release:android:preview": "dotenv -e .env -- npx expo prebuild --platform android && dotenv -e .env -- eas build --platform android --profile preview --local"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -4,14 +4,14 @@ import { Events } from "../lib/analytics";
|
||||
import type { Profile, Couple } from "@/types";
|
||||
|
||||
|
||||
interface AuthState {
|
||||
user: Profile | null;
|
||||
couple: Couple | null;
|
||||
partner: Profile | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
isAnonymous: boolean;
|
||||
|
||||
interface AuthState {
|
||||
user: Profile | null;
|
||||
couple: Couple | null;
|
||||
partner: Profile | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
isAnonymous: boolean;
|
||||
|
||||
|
||||
// Actions
|
||||
fetchUser: () => Promise<void>;
|
||||
@@ -38,19 +38,19 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
user: null,
|
||||
couple: null,
|
||||
partner: null,
|
||||
isLoading: true,
|
||||
isAuthenticated: false,
|
||||
isAnonymous: false,
|
||||
isLoading: true,
|
||||
isAuthenticated: false,
|
||||
isAnonymous: false,
|
||||
|
||||
fetchUser: async () => {
|
||||
try {
|
||||
// First check if we have a session in storage
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (!session?.user) {
|
||||
set({ user: null, isAuthenticated: false, isAnonymous: false, isLoading: false });
|
||||
return;
|
||||
}
|
||||
if (!session?.user) {
|
||||
set({ user: null, isAuthenticated: false, isAnonymous: false, isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the session by fetching the user from the server
|
||||
// This will fail if the session was deleted server-side
|
||||
@@ -59,8 +59,8 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
if (authError || !authUser) {
|
||||
console.log("[Auth] Session invalid, signing out:", authError?.message);
|
||||
|
||||
// Session is invalid - clear everything
|
||||
set({ user: null, couple: null, partner: null, isAuthenticated: false, isAnonymous: false, isLoading: false });
|
||||
// Session is invalid - clear everything
|
||||
set({ user: null, couple: null, partner: null, isAuthenticated: false, isAnonymous: false, isLoading: false });
|
||||
// Clear other stores
|
||||
const { useMatchStore, usePacksStore, useMessageStore, useSubscriptionStore, useNotificationPreferencesStore, useStreakStore } = getOtherStores();
|
||||
useMatchStore.getState().clearMatches();
|
||||
@@ -80,13 +80,20 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
.eq("id", authUser.id)
|
||||
.maybeSingle();
|
||||
|
||||
const isAnonymous = !!(authUser as any).is_anonymous;
|
||||
|
||||
set({
|
||||
user: profile,
|
||||
isAuthenticated: true,
|
||||
isAnonymous,
|
||||
});
|
||||
const isAnonymous = !!(authUser as any).is_anonymous;
|
||||
|
||||
// Only update user if data actually changed to avoid unnecessary re-renders
|
||||
const currentUser = get().user;
|
||||
const userChanged = !currentUser || JSON.stringify(currentUser) !== JSON.stringify(profile);
|
||||
if (userChanged) {
|
||||
set({
|
||||
user: profile,
|
||||
isAuthenticated: true,
|
||||
isAnonymous,
|
||||
});
|
||||
} else if (!get().isAuthenticated) {
|
||||
set({ isAuthenticated: true, isAnonymous });
|
||||
}
|
||||
|
||||
// If user has a couple, fetch couple data; otherwise clear couple/partner
|
||||
if (profile?.couple_id) {
|
||||
@@ -148,14 +155,14 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
Events.signOut();
|
||||
|
||||
// Clear local state FIRST to ensure UI updates even if Supabase call fails
|
||||
set({
|
||||
user: null,
|
||||
couple: null,
|
||||
partner: null,
|
||||
isAuthenticated: false,
|
||||
isAnonymous: false,
|
||||
isLoading: false,
|
||||
});
|
||||
set({
|
||||
user: null,
|
||||
couple: null,
|
||||
partner: null,
|
||||
isAuthenticated: false,
|
||||
isAnonymous: false,
|
||||
isLoading: false,
|
||||
});
|
||||
// Clear other stores
|
||||
const { useMatchStore, usePacksStore, useMessageStore, useSubscriptionStore, useNotificationPreferencesStore, useStreakStore } = getOtherStores();
|
||||
useMatchStore.getState().clearMatches();
|
||||
@@ -178,14 +185,14 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
},
|
||||
|
||||
setUser: (user) => {
|
||||
set({
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
...(user === null && { isAnonymous: false }),
|
||||
isLoading: false,
|
||||
// Clear couple/partner when user is null (signed out)
|
||||
...(user === null && { couple: null, partner: null })
|
||||
});
|
||||
set({
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
...(user === null && { isAnonymous: false }),
|
||||
isLoading: false,
|
||||
// Clear couple/partner when user is null (signed out)
|
||||
...(user === null && { couple: null, partner: null })
|
||||
});
|
||||
// Clear other stores when user signs out
|
||||
if (user === null) {
|
||||
const { useMatchStore, usePacksStore, useMessageStore, useSubscriptionStore, useNotificationPreferencesStore, useStreakStore } = getOtherStores();
|
||||
|
||||
Reference in new issue
Block a user