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:
jjbrunton committed 2026-02-02 19:00:15 +00:00
1 parent de0d667f4f
commit 3afb091e90
8 files changed
+77 -13

No files matched your search

+4
View File
@@ -9,3 +9,7 @@ expo-env.d.ts
*.apk
*.jks
*.keystore
# Fastlane
fastlane-key.json
fastlane/report.xml
fastlane/README.md
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -183,7 +183,7 @@
"backgroundColor": "#1a1a2e"
},
"package": "com.sauci.app",
"versionCode": 44,
"versionCode": 45,
"googleServicesFile": "./google-services.json",
"permissions": [
"RECEIVE_BOOT_COMPLETED",
+13 -10
View File
@@ -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);
+2
View File
@@ -0,0 +1,2 @@
json_key_file(ENV["GOOGLE_PLAY_KEY_FILE"] || File.expand_path("../fastlane-key.json", __dir__))
package_name("com.sauci.app")
+48
View File
@@ -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
+1 -1
View File
@@ -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": {
+7
View File
@@ -82,11 +82,18 @@ export const useAuthStore = create<AuthState>((set, get) => ({
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) {