mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cff928ce5e | ||
|
|
3fc6f8aa1a | ||
|
|
02a7349899 | ||
|
|
8f70dbcc00 | ||
|
|
ac80d6b614 | ||
|
|
056bb62d04 | ||
|
|
80ec1663f9 | ||
|
|
8ff05c050b |
No files matched your search
@@ -59,6 +59,7 @@ e2e/playwright-report/
|
||||
|
||||
# Misc
|
||||
*.pem
|
||||
!apps/mobile/certs/certificate.pem
|
||||
.cache/
|
||||
.temp/
|
||||
.tmp/
|
||||
|
||||
@@ -92,7 +92,7 @@ android {
|
||||
applicationId 'com.sauci.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 48
|
||||
versionCode 49
|
||||
versionName "1.0.7"
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
@@ -182,3 +182,53 @@ dependencies {
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
|
||||
// Reanimated 4.1.7 imports Worklets from its pre-AGP-8 CMake output path,
|
||||
// while AGP 8.14 writes release outputs below intermediates/cxx/RelWithDebInfo.
|
||||
// Keep the checked-in Android project compatible with the supported SDK 54
|
||||
// dependency set by materializing that one legacy input after Worklets builds.
|
||||
def workletsReleaseAbis = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"]
|
||||
def syncWorkletsReleaseCmakeInputs = tasks.register("syncWorkletsReleaseCmakeInputs") {
|
||||
dependsOn(":react-native-worklets:externalNativeBuildRelease")
|
||||
|
||||
doLast {
|
||||
def workletsIntermediates = file("${rootDir}/../../../node_modules/react-native-worklets/android/build/intermediates")
|
||||
workletsReleaseAbis.each { abi ->
|
||||
def libraries = fileTree(workletsIntermediates) {
|
||||
include "cxx/RelWithDebInfo/**/obj/${abi}/libworklets.so"
|
||||
}.files
|
||||
if (libraries.size() != 1) {
|
||||
throw new GradleException("Expected exactly one Worklets release library for ${abi}, found ${libraries.size()}")
|
||||
}
|
||||
def source = libraries.iterator().next()
|
||||
def destination = file("${workletsIntermediates}/cmake/release/obj/${abi}/libworklets.so")
|
||||
destination.parentFile.mkdirs()
|
||||
java.nio.file.Files.deleteIfExists(destination.toPath())
|
||||
source.withInputStream { input ->
|
||||
destination.withOutputStream { output -> output << input }
|
||||
}
|
||||
if (destination.length() != source.length()) {
|
||||
throw new GradleException("Failed to copy Worklets release library for ${abi}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gradle.projectsEvaluated {
|
||||
def reanimatedProject = findProject(":react-native-reanimated")
|
||||
reanimatedProject?.tasks?.configureEach { task ->
|
||||
if (task.name == "externalNativeBuildRelease" || task.name == "prefabReleasePackage") {
|
||||
task.dependsOn(syncWorkletsReleaseCmakeInputs)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.configureEach { task ->
|
||||
if (task.name.startsWith("configureCMakeRelWithDebInfo")) {
|
||||
task.dependsOn(
|
||||
":react-native-worklets:prefabReleasePackage",
|
||||
":react-native-reanimated:prefabReleasePackage",
|
||||
":shopify_react-native-skia:prefabReleasePackage",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
"group.com.sauci.app"
|
||||
]
|
||||
},
|
||||
"buildNumber": "45",
|
||||
"buildNumber": "46",
|
||||
"usesAppleSignIn": true,
|
||||
"googleServicesFile": "./GoogleService-Info.plist",
|
||||
"infoPlist": {
|
||||
@@ -189,7 +189,7 @@
|
||||
"backgroundColor": "#1a1a2e"
|
||||
},
|
||||
"package": "com.sauci.app",
|
||||
"versionCode": 48,
|
||||
"versionCode": 49,
|
||||
"googleServicesFile": "./google-services.json",
|
||||
"permissions": [
|
||||
"RECEIVE_BOOT_COMPLETED",
|
||||
|
||||
@@ -69,10 +69,12 @@ export default function MatchesScreen() {
|
||||
checkNudgeCooldown,
|
||||
} = useMatchStore();
|
||||
const { user, couple, partner } = useAuthStore();
|
||||
const userId = user?.id;
|
||||
const router = useRouter();
|
||||
const { width } = useWindowDimensions();
|
||||
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
||||
const [showTutorial, setShowTutorial] = useState(false);
|
||||
const tutorialDismissedRef = useRef(false);
|
||||
const [nudgeFeedback, setNudgeFeedback] = useState<string | null>(null);
|
||||
const [headerHeight, setHeaderHeight] = useState(130);
|
||||
|
||||
@@ -104,21 +106,30 @@ export default function MatchesScreen() {
|
||||
// Check if tutorial should be shown when screen is focused or matches change
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
let isActive = true;
|
||||
|
||||
const checkTutorial = async () => {
|
||||
if (matches.length > 0) {
|
||||
const seen = await hasSeenMatchesTutorial();
|
||||
if (!seen) {
|
||||
if (matches.length > 0 && userId) {
|
||||
const seen = await hasSeenMatchesTutorial(userId);
|
||||
if (isActive && !tutorialDismissedRef.current && !seen) {
|
||||
setShowTutorial(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
checkTutorial();
|
||||
}, [matches.length])
|
||||
void checkTutorial();
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [matches.length, userId])
|
||||
);
|
||||
|
||||
const handleTutorialComplete = async () => {
|
||||
await markMatchesTutorialSeen();
|
||||
if (!userId) return;
|
||||
|
||||
tutorialDismissedRef.current = true;
|
||||
setShowTutorial(false);
|
||||
await markMatchesTutorialSeen(userId);
|
||||
};
|
||||
|
||||
// Load whichever view is showing, once. Returning to this tab reuses what is
|
||||
|
||||
@@ -772,7 +772,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 44;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -808,7 +808,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ../targets/widget/generated.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 44;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = BXVPBATDZY;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
||||
+114
-114
@@ -1,118 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleAllowMixedLocalizations</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Sauci</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.7</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>app.sauci</string>
|
||||
<string>com.sauci.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>exp+sauci</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.861972684340-0u2as8i5equfhmhmvjihottkf0ig7237</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>45</string>
|
||||
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
|
||||
<false/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>instagram</string>
|
||||
<string>instagram-stories</string>
|
||||
</array>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Sauci uses your camera to take photos and videos that you can share privately with your partner.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Sauci uses Face ID to unlock the app and keep your conversations private.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Sauci uses your microphone to record audio when you capture videos to share privately with your partner.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Sauci saves photos and videos from your partner's messages to your photo library when you tap the save button.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Sauci uses your photo library so you can select photos and videos to share privately with your partner.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
</array>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<false/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Automatic</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleAllowMixedLocalizations</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Sauci</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.7</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>app.sauci</string>
|
||||
<string>com.sauci.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>exp+sauci</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.861972684340-0u2as8i5equfhmhmvjihottkf0ig7237</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>46</string>
|
||||
<key>GOOGLE_ANALYTICS_INAPURCHASE_COLLECTION_ENABLED</key>
|
||||
<false/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>instagram</string>
|
||||
<string>instagram-stories</string>
|
||||
</array>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Sauci uses your camera to take photos and videos that you can share privately with your partner.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Sauci uses Face ID to unlock the app and keep your conversations private.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Sauci uses your microphone to record audio when you capture videos to share privately with your partner.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Sauci saves photos and videos from your partner's messages to your photo library when you tap the save button.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Sauci uses your photo library so you can select photos and videos to share privately with your partner.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
</array>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<false/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Automatic</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,45 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import {
|
||||
hasSeenMatchesTutorial,
|
||||
markMatchesTutorialSeen,
|
||||
resetMatchesTutorial,
|
||||
} from "@/lib/matchesTutorialSeen";
|
||||
|
||||
jest.mock("expo-secure-store", () => ({
|
||||
getItemAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("matchesTutorialSeen", () => {
|
||||
beforeEach(async () => {
|
||||
await AsyncStorage.clear();
|
||||
jest.mocked(SecureStore.getItemAsync).mockReset();
|
||||
jest.mocked(SecureStore.getItemAsync).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it("persists a dismissal for the signed-in account", async () => {
|
||||
await markMatchesTutorialSeen("user-1");
|
||||
|
||||
await expect(hasSeenMatchesTutorial("user-1")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("does not dismiss the tutorial for another account on the same device", async () => {
|
||||
await markMatchesTutorialSeen("user-1");
|
||||
|
||||
await expect(hasSeenMatchesTutorial("user-2")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("migrates an existing legacy dismissal into durable account storage", async () => {
|
||||
jest.mocked(SecureStore.getItemAsync).mockResolvedValue("true");
|
||||
|
||||
await expect(hasSeenMatchesTutorial("user-1")).resolves.toBe(true);
|
||||
await expect(AsyncStorage.getItem("matches_tutorial_seen_user-1")).resolves.toBe("true");
|
||||
});
|
||||
|
||||
it("lets the development reset show the tutorial for the current account again", async () => {
|
||||
await markMatchesTutorialSeen("user-1");
|
||||
await resetMatchesTutorial("user-1");
|
||||
|
||||
await expect(hasSeenMatchesTutorial("user-1")).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -223,7 +223,8 @@ export function ProfileScreen() {
|
||||
label="Reset Matches Tutorial"
|
||||
description="Show the tutorial again"
|
||||
onPress={async () => {
|
||||
await resetMatchesTutorial();
|
||||
if (!user?.id) return;
|
||||
await resetMatchesTutorial(user.id);
|
||||
Alert.alert("Success", "Matches tutorial has been reset.");
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,51 +1,70 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
const STORAGE_KEY = "matches_tutorial_seen";
|
||||
const STORAGE_KEY_PREFIX = "matches_tutorial_seen";
|
||||
const LEGACY_STORAGE_KEY = "matches_tutorial_seen";
|
||||
|
||||
async function getStorage(): Promise<boolean> {
|
||||
const buildKey = (userId: string): string => `${STORAGE_KEY_PREFIX}_${userId}`;
|
||||
|
||||
async function getStorage(userId: string): Promise<boolean> {
|
||||
try {
|
||||
let data: string | null = null;
|
||||
const key = buildKey(userId);
|
||||
|
||||
if (Platform.OS === "web") {
|
||||
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
||||
data = window.localStorage.getItem(STORAGE_KEY);
|
||||
return window.localStorage.getItem(key) === "true";
|
||||
}
|
||||
} else {
|
||||
data = await SecureStore.getItemAsync(STORAGE_KEY);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return data === "true";
|
||||
const storedValue = await AsyncStorage.getItem(key);
|
||||
if (storedValue !== null) {
|
||||
return storedValue === "true";
|
||||
}
|
||||
|
||||
// Keep dismissals made before the account-scoped AsyncStorage key was
|
||||
// introduced. Future reads use the durable app storage key above.
|
||||
const legacyValue = await SecureStore.getItemAsync(LEGACY_STORAGE_KEY);
|
||||
if (legacyValue === "true") {
|
||||
await AsyncStorage.setItem(key, "true");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error reading matches tutorial state:", error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function setStorage(seen: boolean): Promise<void> {
|
||||
async function setStorage(userId: string, seen: boolean): Promise<void> {
|
||||
try {
|
||||
const data = seen ? "true" : "false";
|
||||
const key = buildKey(userId);
|
||||
|
||||
if (Platform.OS === "web") {
|
||||
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
||||
window.localStorage.setItem(STORAGE_KEY, data);
|
||||
window.localStorage.setItem(key, data);
|
||||
}
|
||||
} else {
|
||||
await SecureStore.setItemAsync(STORAGE_KEY, data);
|
||||
await AsyncStorage.setItem(key, data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving matches tutorial state:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function hasSeenMatchesTutorial(): Promise<boolean> {
|
||||
return getStorage();
|
||||
/** The Matches tutorial is shown at most once per signed-in account. */
|
||||
export async function hasSeenMatchesTutorial(userId: string): Promise<boolean> {
|
||||
return getStorage(userId);
|
||||
}
|
||||
|
||||
export async function markMatchesTutorialSeen(): Promise<void> {
|
||||
await setStorage(true);
|
||||
export async function markMatchesTutorialSeen(userId: string): Promise<void> {
|
||||
await setStorage(userId, true);
|
||||
}
|
||||
|
||||
export async function resetMatchesTutorial(): Promise<void> {
|
||||
await setStorage(false);
|
||||
export async function resetMatchesTutorial(userId: string): Promise<void> {
|
||||
await setStorage(userId, false);
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>45</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.7</string>
|
||||
<string>46</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.7</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,5 +1,16 @@
|
||||
# Sauci Mobile Release Notes
|
||||
|
||||
## v1.0.7 - 31 August 2026
|
||||
_Android build 49 · iOS build 46_
|
||||
|
||||
More ways to discover each other, play together, and keep your shared space moving.
|
||||
|
||||
- **A new way to compare notes** - Take a couples quiz, see how well you know each other, and share your score when you are ready.
|
||||
- **Dares with a little more proof** - Photo and voice-note proof give you new ways to bring a dare to life for your partner.
|
||||
- **Your answers can wait safely** - Keep answering before you pair; your saved answers stay sealed until your partner joins.
|
||||
- **An easier way to get started together** - A gentler welcome and clearer invite links help you find the right first prompt and bring your partner in.
|
||||
- **More dependable updates** - The app is ready to receive signed over-the-air improvements more smoothly.
|
||||
|
||||
## v1.0.6 — 30 August 2026
|
||||
_iOS build 44 · Android build 48_
|
||||
|
||||
|
||||
Reference in new issue
Block a user