Compare commits

...
5 Commits
Author SHA1 Message Date
soconnor 5fa30f365f Make iOS release signing deterministic 2026-08-15 14:18:09 -04:00
soconnor b9c7fb325a Add in-app account deletion 2026-08-15 13:46:47 -04:00
soconnor 9eadb0d7fb Document public App Store demo login 2026-08-15 00:17:57 -04:00
soconnor fa42ffb767 Add App Store review screenshots 2026-08-14 19:48:21 -04:00
soconnor d2fa050c05 Bump iOS build to 27 2026-08-14 18:51:49 -04:00
20 changed files with 284 additions and 60 deletions
+3
View File
@@ -9,6 +9,9 @@ APPLE_TEAM_ID=
# #
# If export fails with "profile doesn't include signing certificate", regenerate App Store # If export fails with "profile doesn't include signing certificate", regenerate App Store
# profiles at developer.apple.com for com.beenvoice.app and com.beenvoice.app.ExpoWidgetsTarget, # profiles at developer.apple.com for com.beenvoice.app and com.beenvoice.app.ExpoWidgetsTarget,
# Optional overrides when those App Store profile names differ from the bundle IDs:
# IOS_MAIN_APPSTORE_PROFILE_NAME=com.beenvoice.app
# IOS_WIDGET_APPSTORE_PROFILE_NAME=com.beenvoice.app.ExpoWidgetsTarget
# then re-run the full release (not --export-only). # then re-run the full release (not --export-only).
# Production API baked into the JS bundle (App Store / TestFlight) # Production API baked into the JS bundle (App Store / TestFlight)
+1 -1
View File
@@ -10,7 +10,7 @@
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
"bundleIdentifier": "com.beenvoice.app", "bundleIdentifier": "com.beenvoice.app",
"buildNumber": "26", "buildNumber": "28",
"icon": "./assets/beenvoice.icon", "icon": "./assets/beenvoice.icon",
"infoPlist": { "infoPlist": {
"ITSAppUsesNonExemptEncryption": false, "ITSAppUsesNonExemptEncryption": false,
+189 -30
View File
@@ -2,7 +2,15 @@ import { useState } from "react";
import Constants from "expo-constants"; import Constants from "expo-constants";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router"; import { router } from "expo-router";
import { Alert, Platform, Pressable, StyleSheet, Switch, Text, View } from "react-native"; import {
Alert,
Platform,
Pressable,
StyleSheet,
Switch,
Text,
View,
} from "react-native";
import { TabPage } from "@/components/TabPage"; import { TabPage } from "@/components/TabPage";
import { TabScrollView } from "@/components/TabScrollView"; import { TabScrollView } from "@/components/TabScrollView";
@@ -20,7 +28,10 @@ import { useAppLock } from "@/contexts/AppLockContext";
import { useAuthClient, useSession } from "@/contexts/AuthContext"; import { useAuthClient, useSession } from "@/contexts/AuthContext";
import { type ColorMode, useAppTheme } from "@/contexts/ThemeContext"; import { type ColorMode, useAppTheme } from "@/contexts/ThemeContext";
import { startAdditionalAccountSignIn } from "@/lib/add-account"; import { startAdditionalAccountSignIn } from "@/lib/add-account";
import { confirmRemoveAccount, finishAccountRemoval } from "@/lib/account-actions"; import {
confirmRemoveAccount,
finishAccountRemoval,
} from "@/lib/account-actions";
import { performAuthReset } from "@/lib/auth-session"; import { performAuthReset } from "@/lib/auth-session";
import { api } from "@/lib/trpc"; import { api } from "@/lib/trpc";
@@ -61,6 +72,7 @@ export default function SettingsScreen() {
lock, lock,
} = useAppLock(); } = useAppLock();
const profileQuery = api.settings.getProfile.useQuery(); const profileQuery = api.settings.getProfile.useQuery();
const deleteAccountMutation = api.settings.deleteAccount.useMutation();
const [pinPrompt, setPinPrompt] = useState< const [pinPrompt, setPinPrompt] = useState<
| { mode: "create" } | { mode: "create" }
@@ -110,10 +122,54 @@ export default function SettingsScreen() {
function confirmSignOut() { function confirmSignOut() {
Alert.alert("Sign out", "Sign out of this account on this device?", [ Alert.alert("Sign out", "Sign out of this account on this device?", [
{ text: "Cancel", style: "cancel" }, { text: "Cancel", style: "cancel" },
{ text: "Sign out", style: "destructive", onPress: () => void handleSignOut() }, {
text: "Sign out",
style: "destructive",
onPress: () => void handleSignOut(),
},
]); ]);
} }
async function handleDeleteAccount() {
if (!activeAccountId) return;
try {
await deleteAccountMutation.mutateAsync({
confirmText: "DELETE MY ACCOUNT",
});
const result = await removeAccount(activeAccountId);
await finishAccountRemoval({
result,
authClient,
clearActiveAccount,
activeAccountId,
});
if (result.remainingCount > 0) {
router.replace("/(auth)/select-account");
}
} catch (error) {
Alert.alert(
"Could not delete account",
error instanceof Error ? error.message : "Please try again.",
);
}
}
function confirmDeleteAccount() {
Alert.alert(
"Permanently delete account?",
"This deletes your account, invoices, clients, businesses, expenses, time entries, uploaded files, and sign-in data. This cannot be undone.",
[
{ text: "Cancel", style: "cancel" },
{
text: "Delete Account",
style: "destructive",
onPress: () => void handleDeleteAccount(),
},
],
);
}
function confirmInstanceChange() { function confirmInstanceChange() {
Alert.alert( Alert.alert(
"Server updated", "Server updated",
@@ -145,7 +201,10 @@ export default function SettingsScreen() {
await enableLock(pin); await enableLock(pin);
setPinPrompt(null); setPinPrompt(null);
} catch (err) { } catch (err) {
Alert.alert("Could not enable lock", err instanceof Error ? err.message : "Try again"); Alert.alert(
"Could not enable lock",
err instanceof Error ? err.message : "Try again",
);
} }
return; return;
} }
@@ -169,7 +228,10 @@ export default function SettingsScreen() {
if (pinPrompt?.mode === "change-next") { if (pinPrompt?.mode === "change-next") {
const success = await changePin(pendingPin, pin); const success = await changePin(pendingPin, pin);
if (!success) { if (!success) {
Alert.alert("Could not change PIN", "Check your current PIN and try again."); Alert.alert(
"Could not change PIN",
"Check your current PIN and try again.",
);
return; return;
} }
setPendingPin(""); setPendingPin("");
@@ -207,9 +269,13 @@ export default function SettingsScreen() {
: "Enter your current PIN." : "Enter your current PIN."
} }
confirmLabel={ confirmLabel={
pinPrompt?.mode === "create" || pinPrompt?.mode === "change-next" ? "Save" : "Continue" pinPrompt?.mode === "create" || pinPrompt?.mode === "change-next"
? "Save"
: "Continue"
}
requireConfirmation={
pinPrompt?.mode === "create" || pinPrompt?.mode === "change-next"
} }
requireConfirmation={pinPrompt?.mode === "create" || pinPrompt?.mode === "change-next"}
onCancel={() => { onCancel={() => {
setPendingPin(""); setPendingPin("");
setPinPrompt(null); setPinPrompt(null);
@@ -218,7 +284,10 @@ export default function SettingsScreen() {
/> />
<TabScrollView <TabScrollView
header={ header={
<PageHeader title="Settings" subtitle="Account and app preferences" /> <PageHeader
title="Settings"
subtitle="Account and app preferences"
/>
} }
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
> >
@@ -253,21 +322,43 @@ export default function SettingsScreen() {
<Pressable <Pressable
accessibilityRole="button" accessibilityRole="button"
onPress={() => void switchAccount(account.id)} onPress={() => void switchAccount(account.id)}
style={({ pressed }) => [styles.accountMain, pressed && styles.pressed]} style={({ pressed }) => [
styles.accountMain,
pressed && styles.pressed,
]}
> >
<View style={styles.accountMeta}> <View style={styles.accountMeta}>
<Text style={[styles.accountName, { color: colors.foreground }]}> <Text
style={[
styles.accountName,
{ color: colors.foreground },
]}
>
{account.name || account.email} {account.name || account.email}
</Text> </Text>
<Text style={[styles.accountSub, { color: colors.mutedForeground }]}> <Text
style={[
styles.accountSub,
{ color: colors.mutedForeground },
]}
>
{account.email} {account.email}
</Text> </Text>
<Text style={[styles.accountSub, { color: colors.mutedForeground }]}> <Text
style={[
styles.accountSub,
{ color: colors.mutedForeground },
]}
>
{account.instanceUrl.replace(/^https?:\/\//, "")} {account.instanceUrl.replace(/^https?:\/\//, "")}
</Text> </Text>
</View> </View>
{isActive ? ( {isActive ? (
<Text style={[styles.activeBadge, { color: colors.primary }]}>Active</Text> <Text
style={[styles.activeBadge, { color: colors.primary }]}
>
Active
</Text>
) : null} ) : null}
</Pressable> </Pressable>
<Pressable <Pressable
@@ -275,11 +366,21 @@ export default function SettingsScreen() {
accessibilityLabel={`Remove ${account.name || account.email}`} accessibilityLabel={`Remove ${account.name || account.email}`}
hitSlop={8} hitSlop={8}
onPress={() => onPress={() =>
handleRemoveAccount(account.id, account.name || account.email) handleRemoveAccount(
account.id,
account.name || account.email,
)
} }
style={({ pressed }) => [styles.removeButton, pressed && styles.pressed]} style={({ pressed }) => [
styles.removeButton,
pressed && styles.pressed,
]}
> >
<Ionicons name="trash-outline" size={18} color={colors.destructive} /> <Ionicons
name="trash-outline"
size={18}
color={colors.destructive}
/>
</Pressable> </Pressable>
</View> </View>
); );
@@ -293,10 +394,13 @@ export default function SettingsScreen() {
<Button <Button
title="Add another account" title="Add another account"
variant="secondary" variant="secondary"
onPress={() => void startAdditionalAccountSignIn(clearActiveAccount)} onPress={() =>
void startAdditionalAccountSignIn(clearActiveAccount)
}
/> />
<Text style={[styles.meta, { color: colors.mutedForeground }]}> <Text style={[styles.meta, { color: colors.mutedForeground }]}>
Tap an account to switch. Refresh updates names from saved sign-in data. Tap an account to switch. Refresh updates names from saved sign-in
data.
</Text> </Text>
</Card> </Card>
@@ -309,7 +413,11 @@ export default function SettingsScreen() {
<Card title="Security"> <Card title="Security">
<View style={styles.settingRow}> <View style={styles.settingRow}>
<View style={styles.settingCopy}> <View style={styles.settingCopy}>
<Text style={[styles.settingTitle, { color: colors.foreground }]}>App lock</Text> <Text
style={[styles.settingTitle, { color: colors.foreground }]}
>
App lock
</Text>
<Text style={[styles.meta, { color: colors.mutedForeground }]}> <Text style={[styles.meta, { color: colors.mutedForeground }]}>
Require a PIN when reopening the app Require a PIN when reopening the app
</Text> </Text>
@@ -324,10 +432,14 @@ export default function SettingsScreen() {
{lockEnabled && biometricAvailable ? ( {lockEnabled && biometricAvailable ? (
<View style={styles.settingRow}> <View style={styles.settingRow}>
<View style={styles.settingCopy}> <View style={styles.settingCopy}>
<Text style={[styles.settingTitle, { color: colors.foreground }]}> <Text
style={[styles.settingTitle, { color: colors.foreground }]}
>
{biometricLabel} {biometricLabel}
</Text> </Text>
<Text style={[styles.meta, { color: colors.mutedForeground }]}> <Text
style={[styles.meta, { color: colors.mutedForeground }]}
>
Unlock with {biometricLabel.toLowerCase()} when available Unlock with {biometricLabel.toLowerCase()} when available
</Text> </Text>
</View> </View>
@@ -341,7 +453,11 @@ export default function SettingsScreen() {
{lockEnabled ? ( {lockEnabled ? (
<> <>
<Button title="Change PIN" variant="secondary" onPress={handleChangePin} /> <Button
title="Change PIN"
variant="secondary"
onPress={handleChangePin}
/>
<Button title="Lock now" variant="secondary" onPress={lock} /> <Button title="Lock now" variant="secondary" onPress={lock} />
</> </>
) : null} ) : null}
@@ -360,14 +476,20 @@ export default function SettingsScreen() {
styles.themeChip, styles.themeChip,
{ {
borderColor: selected ? colors.primary : colors.border, borderColor: selected ? colors.primary : colors.border,
backgroundColor: selected ? colors.muted : "transparent", backgroundColor: selected
? colors.muted
: "transparent",
}, },
]} ]}
> >
<Text <Text
style={[ style={[
styles.themeChipLabel, styles.themeChipLabel,
{ color: selected ? colors.foreground : colors.mutedForeground }, {
color: selected
? colors.foreground
: colors.mutedForeground,
},
]} ]}
> >
{option.label} {option.label}
@@ -380,24 +502,52 @@ export default function SettingsScreen() {
<Card title="App"> <Card title="App">
<View style={styles.appRow}> <View style={styles.appRow}>
<Text style={[styles.meta, { color: colors.mutedForeground }]}>Version</Text> <Text style={[styles.meta, { color: colors.mutedForeground }]}>
<Text style={[styles.appValue, { color: colors.foreground }]}>{appVersion}</Text> Version
</Text>
<Text style={[styles.appValue, { color: colors.foreground }]}>
{appVersion}
</Text>
</View> </View>
<View style={styles.appRow}> <View style={styles.appRow}>
<Text style={[styles.meta, { color: colors.mutedForeground }]}>Platform</Text> <Text style={[styles.meta, { color: colors.mutedForeground }]}>
Platform
</Text>
<Text style={[styles.appValue, { color: colors.foreground }]}> <Text style={[styles.appValue, { color: colors.foreground }]}>
{Constants.platform?.ios ? "iOS" : "Other"} {Constants.platform?.ios ? "iOS" : "Other"}
</Text> </Text>
</View> </View>
</Card> </Card>
<Card title="Delete account">
<Text style={[styles.meta, { color: colors.mutedForeground }]}>
Permanently delete this account and all of its data from the
server. This cannot be undone.
</Text>
<Button
title={
deleteAccountMutation.isPending
? "Deleting account…"
: "Delete Account"
}
variant="danger"
loading={deleteAccountMutation.isPending}
disabled={deleteAccountMutation.isPending}
onPress={confirmDeleteAccount}
/>
</Card>
<Pressable <Pressable
accessibilityRole="button" accessibilityRole="button"
accessibilityState={{ expanded: showAdvanced }} accessibilityState={{ expanded: showAdvanced }}
onPress={() => setShowAdvanced((open) => !open)} onPress={() => setShowAdvanced((open) => !open)}
style={styles.advancedToggle} style={styles.advancedToggle}
> >
<Text style={[styles.advancedLabel, { color: colors.mutedForeground }]}>Advanced</Text> <Text
style={[styles.advancedLabel, { color: colors.mutedForeground }]}
>
Advanced
</Text>
<Ionicons <Ionicons
name={showAdvanced ? "chevron-up" : "chevron-down"} name={showAdvanced ? "chevron-up" : "chevron-down"}
size={16} size={16}
@@ -408,14 +558,23 @@ export default function SettingsScreen() {
{showAdvanced ? ( {showAdvanced ? (
<Card title="Server instance"> <Card title="Server instance">
<InstanceUrlField onSaved={confirmInstanceChange} /> <InstanceUrlField onSaved={confirmInstanceChange} />
<Text style={[styles.currentServer, { color: colors.mutedForeground }]}> <Text
style={[
styles.currentServer,
{ color: colors.mutedForeground },
]}
>
Connected to {activeAccount?.instanceUrl ?? apiUrl} Connected to {activeAccount?.instanceUrl ?? apiUrl}
</Text> </Text>
</Card> </Card>
) : null} ) : null}
<View style={styles.actions}> <View style={styles.actions}>
<Button title="Sign Out" variant="danger" onPress={confirmSignOut} /> <Button
title="Sign Out"
variant="danger"
onPress={confirmSignOut}
/>
</View> </View>
</TabScrollView> </TabScrollView>
</TabPage> </TabPage>
+10 -2
View File
@@ -116,12 +116,12 @@ Initial App Store release.
### Demo account (production server) ### Demo account (production server)
Ensure migration `0014_seed_demo_account` has run on the server reviewers will hit. Ensure migrations through `0028_enable_public_demo_password` have run on the server reviewers will hit.
| Field | Value | | Field | Value |
|-------|--------| |-------|--------|
| **Username** | `demo@example.com` | | **Username** | `demo@example.com` |
| **Password** | Provision a private credential with `bun run demo:provision` in the web repository, then enter it only in App Store Connect | | **Password** | `demo123` |
### Notes for Review ### Notes for Review
@@ -139,6 +139,13 @@ WHAT TO TEST
• Invoices — list includes draft, sent, and paid examples. • Invoices — list includes draft, sent, and paid examples.
• Settings — profile, theme, optional app lock (PIN / Face ID). • Settings — profile, theme, optional app lock (PIN / Face ID).
ACCOUNT DELETION
Settings → Delete account offers permanent in-app account deletion without contacting support.
After a destructive confirmation, the server removes the account record, sessions, access keys,
invoices, clients, businesses, recurring invoices, expenses and receipts, time entries, templates,
audit records, and uploaded files. The app then removes the local account and returns to sign-in.
The supplied demo account is shared, so please test deletion last if deletion verification is required.
APP LOCK APP LOCK
Optional. Enable in Settings → App Lock. Face ID uses on-device biometrics only; no biometric data is sent to our servers. Optional. Enable in Settings → App Lock. Face ID uses on-device biometrics only; no biometric data is sent to our servers.
@@ -276,6 +283,7 @@ Prerequisites:
- [ ] TestFlight smoke test on device (login, timer, invoices, app lock) - [ ] TestFlight smoke test on device (login, timer, invoices, app lock)
- [ ] Live Activity tested on physical iPhone - [ ] Live Activity tested on physical iPhone
- [ ] App Privacy answers match actual data flows - [ ] App Privacy answers match actual data flows
- [ ] In-app account deletion succeeds from Settings and returns to sign-in
- [ ] Screenshots uploaded for required device sizes - [ ] Screenshots uploaded for required device sizes
- [ ] Review notes include demo credentials and server URL - [ ] Review notes include demo credentials and server URL
- [ ] Export compliance answered - [ ] Export compliance answered
+48 -26
View File
@@ -2,41 +2,63 @@
const { withXcodeProject } = require("@expo/config-plugins"); const { withXcodeProject } = require("@expo/config-plugins");
const RELEASE_SIGN_KEY = '"CODE_SIGN_IDENTITY[sdk=iphoneos*]"'; const RELEASE_SIGN_KEY = '"CODE_SIGN_IDENTITY[sdk=iphoneos*]"';
const MAIN_BUNDLE_ID = "com.beenvoice.app";
const WIDGET_BUNDLE_ID = "com.beenvoice.app.ExpoWidgetsTarget";
/** /**
* RN / Expo sets Release CODE_SIGN_IDENTITY to "iPhone Developer", which forces * Keep App Store archives on distribution signing. Automatic signing can select
* development-signed archives. Remove it so automatic signing picks Distribution * an Apple Development identity for the widget target when archiving from the CLI,
* for App Store archives. * so both release targets use their explicit App Store profiles instead.
*/ */
function configureReleaseSigning(project) {
const configurations = project.pbxXCBuildConfigurationSection();
for (const key of Object.keys(configurations)) {
const buildConfig = configurations[key];
if (
!buildConfig ||
typeof buildConfig !== "object" ||
!buildConfig.buildSettings
) {
continue;
}
if (buildConfig.name !== "Release") {
continue;
}
const bundleId = String(
buildConfig.buildSettings.PRODUCT_BUNDLE_IDENTIFIER ?? "",
).replaceAll('"', "");
if (bundleId !== MAIN_BUNDLE_ID && bundleId !== WIDGET_BUNDLE_ID) {
continue;
}
const profileName =
bundleId === WIDGET_BUNDLE_ID
? (process.env.IOS_WIDGET_APPSTORE_PROFILE_NAME ?? WIDGET_BUNDLE_ID)
: (process.env.IOS_MAIN_APPSTORE_PROFILE_NAME ?? MAIN_BUNDLE_ID);
buildConfig.buildSettings[RELEASE_SIGN_KEY] = '"Apple Distribution"';
buildConfig.buildSettings.CODE_SIGN_STYLE = "Manual";
buildConfig.buildSettings.PROVISIONING_PROFILE_SPECIFIER = `"${profileName}"`;
if (process.env.APPLE_TEAM_ID) {
buildConfig.buildSettings.DEVELOPMENT_TEAM = process.env.APPLE_TEAM_ID;
}
}
return project;
}
/** @type {import('@expo/config-plugins').ConfigPlugin} */ /** @type {import('@expo/config-plugins').ConfigPlugin} */
function withAppStoreSigning(config) { function withAppStoreSigning(config) {
return withXcodeProject(config, (config) => { return withXcodeProject(config, (config) => {
const project = config.modResults; configureReleaseSigning(config.modResults);
const configurations = project.pbxXCBuildConfigurationSection();
for (const key of Object.keys(configurations)) {
const buildConfig = configurations[key];
if (!buildConfig || typeof buildConfig !== "object" || !buildConfig.buildSettings) {
continue;
}
if (buildConfig.name !== "Release") {
continue;
}
const identity = buildConfig.buildSettings[RELEASE_SIGN_KEY];
if (
identity === "iPhone Developer" ||
identity === '"iPhone Developer"' ||
identity === "Apple Distribution" ||
identity === '"Apple Distribution"'
) {
delete buildConfig.buildSettings[RELEASE_SIGN_KEY];
}
}
return config; return config;
}); });
} }
module.exports = withAppStoreSigning; module.exports = withAppStoreSigning;
module.exports.configureReleaseSigning = configureReleaseSigning;
+20
View File
@@ -0,0 +1,20 @@
// @ts-check
const fs = require("fs");
const path = require("path");
const xcode = require("xcode");
const { configureReleaseSigning } = require("../plugins/withAppStoreSigning");
const projectPath = process.argv[2];
if (!projectPath) {
throw new Error(
"Usage: node scripts/configure-ios-signing.js <project.pbxproj>",
);
}
const resolvedPath = path.resolve(projectPath);
const project = xcode.project(resolvedPath);
project.parseSync();
configureReleaseSigning(project);
fs.writeFileSync(resolvedPath, project.writeSync());
console.log("Configured manual App Store signing for iOS release targets.");
+4 -1
View File
@@ -185,6 +185,10 @@ prepare_native_project() {
fi fi
) )
if [[ -f "$ROOT/$PROJECT/project.pbxproj" ]]; then
node "$ROOT/scripts/configure-ios-signing.js" "$ROOT/$PROJECT/project.pbxproj"
fi
resolve_xcode_workspace resolve_xcode_workspace
} }
@@ -241,7 +245,6 @@ archive_app() {
-archivePath "$ARCHIVE_PATH" \ -archivePath "$ARCHIVE_PATH" \
-destination "generic/platform=iOS" \ -destination "generic/platform=iOS" \
-allowProvisioningUpdates \ -allowProvisioningUpdates \
CODE_SIGN_STYLE=Automatic \
DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \ DEVELOPMENT_TEAM="$APPLE_TEAM_ID" \
"${API_AUTH_ARGS[@]}" \ "${API_AUTH_ARGS[@]}" \
archive archive
+9
View File
@@ -0,0 +1,9 @@
# App Store screenshots — v1.0
Generated from the current Expo app using the seeded local review workspace.
- `final/iphone-69`: five 1320×2868 screenshots uploaded to the App Store Connect `APP_IPHONE_67` set.
- `final/ipad-129`: one 2064×2752 screenshot uploaded to the App Store Connect `APP_IPAD_PRO_3GEN_129` set.
- `raw`: unframed simulator captures used to create the final artwork.
The final artwork uses the Black Titanium iPhone 16 Pro Max and Space Black iPad Pro 13-inch (M5) PNG frames supplied in `~/Downloads`. The frame source files are not duplicated in this repository.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB