Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lock-app/LockApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SDKROOT = auto;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
Expand Down Expand Up @@ -260,7 +260,7 @@
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SDKROOT = auto;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
Expand All @@ -271,6 +271,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = LockApp/LockApp.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6MZBUNDJGM;
Expand Down Expand Up @@ -301,6 +302,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = LockApp/LockApp.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6MZBUNDJGM;
Expand Down
13 changes: 12 additions & 1 deletion lock-app/LockApp/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import SwiftUI

extension View {
@ViewBuilder
func lockAppStatusBarHidden() -> some View {
#if os(iOS)
self.statusBar(hidden: true)
#else
self
#endif
}
}

struct LockScreenView: View {
// アプリ名はここで変更できます
let appName: String = "Instagram"
Expand Down Expand Up @@ -98,7 +109,7 @@ struct LockScreenView: View {
.background(Color.black)
}
}
.statusBar(hidden: true)
.lockAppStatusBarHidden()
}
}

Expand Down
10 changes: 10 additions & 0 deletions lock-app/LockApp/LockApp.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?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>com.apple.security.application-groups</key>
<array>
<string>group.com.yohaku.shared</string>
</array>
</dict>
</plist>
30 changes: 30 additions & 0 deletions lock-app/LockApp/LockManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ import Combine
class LockManager: ObservableObject {
static let shared = LockManager()

private let appGroupId = "group.com.yohaku.shared"
private let defaults = UserDefaults.standard
private var sharedDefaults: UserDefaults? {
UserDefaults(suiteName: appGroupId)
}
private let usageKey = "lockapp.usage_seconds"
private let limitKey = "lockapp.limit_seconds"
private let dateKey = "lockapp.date"
private let syncEnabledKey = "yohaku.syncEnabled"
private let sharedUsageKey = "yohaku.todayUsageSeconds"
private let sharedLimitKey = "yohaku.dailyLimitSeconds"
private let sharedBlockedKey = "yohaku.isBlocked"
private let sharedDateKey = "yohaku.localDate"

@Published var usageSeconds: Int = 0
@Published var limitSeconds: Int = 3600 // Default: 1 hour
Expand All @@ -20,6 +29,16 @@ class LockManager: ObservableObject {
}

private func loadState() {
if let shared = sharedDefaults,
shared.bool(forKey: syncEnabledKey) {
usageSeconds = shared.integer(forKey: sharedUsageKey)
let sharedLimit = shared.integer(forKey: sharedLimitKey)
limitSeconds = sharedLimit > 0 ? sharedLimit : 3600
currentDate = shared.string(forKey: sharedDateKey) ?? getTodayString()
isLocked = shared.bool(forKey: sharedBlockedKey)
return
}

usageSeconds = defaults.integer(forKey: usageKey)
limitSeconds = defaults.integer(forKey: limitKey)
if limitSeconds == 0 {
Expand All @@ -42,6 +61,11 @@ class LockManager: ObservableObject {
}

private func checkDateChange() {
if let shared = sharedDefaults,
shared.bool(forKey: syncEnabledKey) {
// In synced mode, source of truth is mobile app state.
return
}
let today = getTodayString()
if currentDate != today {
// New day - reset usage
Expand All @@ -58,6 +82,12 @@ class LockManager: ObservableObject {

// MARK: - Public Methods

func refreshState() {
loadState()
checkDateChange()
updateLockStatus()
}

func setLimit(seconds: Int) {
limitSeconds = max(60, seconds) // Minimum 1 minute
saveState()
Expand Down
49 changes: 30 additions & 19 deletions lock-app/LockApp/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,39 @@ import SwiftUI
struct MainView: View {
@StateObject private var lockManager = LockManager.shared
@State private var showSettings = false
@Environment(\.scenePhase) private var scenePhase

var body: some View {
if lockManager.isLocked {
LockScreenView()
.overlay(
Button(action: { showSettings = true }) {
Image(systemName: "gearshape.fill")
.foregroundColor(.gray.opacity(0.5))
.padding()
Group {
if lockManager.isLocked {
LockScreenView()
.overlay(
Button(action: { showSettings = true }) {
Image(systemName: "gearshape.fill")
.foregroundColor(.gray.opacity(0.5))
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(.top, 60)
.padding(.leading, 20)
)
.sheet(isPresented: $showSettings) {
SettingsView()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(.top, 60)
.padding(.leading, 20)
)
.sheet(isPresented: $showSettings) {
SettingsView()
}
} else {
UnlockedView()
.sheet(isPresented: $showSettings) {
SettingsView()
}
} else {
UnlockedView()
.sheet(isPresented: $showSettings) {
SettingsView()
}
}
}
.onAppear {
lockManager.refreshState()
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .active {
lockManager.refreshState()
}
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions mobile/ios/mobile.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
0C80B921A6F3F58F76C31292 /* libPods-mobile.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-mobile.a */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
69E8CF1C2BD56AE7BB01FC5D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
6A21C001E9434CB3BFC3A101 /* SharedLockStateModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A21C001E9434CB3BFC3A102 /* SharedLockStateModule.swift */; };
6A21C001E9434CB3BFC3A103 /* SharedLockStateModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A21C001E9434CB3BFC3A104 /* SharedLockStateModule.m */; };
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
Expand All @@ -22,6 +24,8 @@
3B4392A12AC88292D35C810B /* Pods-mobile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-mobile.debug.xcconfig"; path = "Target Support Files/Pods-mobile/Pods-mobile.debug.xcconfig"; sourceTree = "<group>"; };
5709B34CF0A7D63546082F79 /* Pods-mobile.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-mobile.release.xcconfig"; path = "Target Support Files/Pods-mobile/Pods-mobile.release.xcconfig"; sourceTree = "<group>"; };
5DCACB8F33CDC322A6C60F78 /* libPods-mobile.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-mobile.a"; sourceTree = BUILT_PRODUCTS_DIR; };
6A21C001E9434CB3BFC3A102 /* SharedLockStateModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SharedLockStateModule.swift; path = mobile/SharedLockStateModule.swift; sourceTree = "<group>"; };
6A21C001E9434CB3BFC3A104 /* SharedLockStateModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SharedLockStateModule.m; path = mobile/SharedLockStateModule.m; sourceTree = "<group>"; };
761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = mobile/AppDelegate.swift; sourceTree = "<group>"; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = mobile/LaunchScreen.storyboard; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
Expand All @@ -44,6 +48,8 @@
children = (
13B07FB51A68108700A75B9A /* Images.xcassets */,
761780EC2CA45674006654EE /* AppDelegate.swift */,
6A21C001E9434CB3BFC3A102 /* SharedLockStateModule.swift */,
6A21C001E9434CB3BFC3A104 /* SharedLockStateModule.m */,
13B07FB61A68108700A75B9A /* Info.plist */,
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
Expand Down Expand Up @@ -246,6 +252,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6A21C001E9434CB3BFC3A101 /* SharedLockStateModule.swift in Sources */,
6A21C001E9434CB3BFC3A103 /* SharedLockStateModule.m in Sources */,
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -258,6 +266,7 @@
baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-mobile.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = mobile/mobile.entitlements;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = U9FDK2B385;
Expand Down Expand Up @@ -288,6 +297,7 @@
baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-mobile.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = mobile/mobile.entitlements;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 1;
INFOPLIST_FILE = mobile/Info.plist;
Expand Down
9 changes: 9 additions & 0 deletions mobile/ios/mobile/SharedLockStateModule.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(SharedLockStateModule, NSObject)

RCT_EXTERN_METHOD(saveLockState:(NSDictionary *)payload
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

@end
33 changes: 33 additions & 0 deletions mobile/ios/mobile/SharedLockStateModule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation
import React

@objc(SharedLockStateModule)
class SharedLockStateModule: NSObject {
private let groupId = "group.com.yohaku.shared"

@objc
func saveLockState(
_ payload: NSDictionary,
resolver resolve: RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock
) {
guard let sharedDefaults = UserDefaults(suiteName: groupId) else {
reject("shared_defaults_unavailable", "Failed to access shared UserDefaults", nil)
return
}

sharedDefaults.set(payload["syncEnabled"] as? Bool ?? false, forKey: "yohaku.syncEnabled")
sharedDefaults.set(payload["dailyLimitSeconds"] as? Int ?? 0, forKey: "yohaku.dailyLimitSeconds")
sharedDefaults.set(payload["todayUsageSeconds"] as? Int ?? 0, forKey: "yohaku.todayUsageSeconds")
sharedDefaults.set(payload["isBlocked"] as? Bool ?? false, forKey: "yohaku.isBlocked")
sharedDefaults.set(payload["localDate"] as? String ?? "", forKey: "yohaku.localDate")
sharedDefaults.set(payload["updatedAt"] as? String ?? "", forKey: "yohaku.updatedAt")

resolve(nil)
}

@objc
static func requiresMainQueueSetup() -> Bool {
return false
}
}
10 changes: 10 additions & 0 deletions mobile/ios/mobile/mobile.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?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>com.apple.security.application-groups</key>
<array>
<string>group.com.yohaku.shared</string>
</array>
</dict>
</plist>
42 changes: 42 additions & 0 deletions mobile/src/lib/app-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { mockStore } from './mock-store';
import { supabase } from './supabase';
import { notifyIfThresholdReached } from './usage-warning-notifier';
import { SharedLockState } from '../native/shared-lock-state';
import { env } from '../config/env';

interface AppContextType {
Expand Down Expand Up @@ -105,7 +106,7 @@
const [pendingPaymentMethodId, setPendingPaymentMethodId] = useState<
string | null
>(null);
// eslint-disable-next-line @typescript-eslint/no-unused-vars

Check warning on line 109 in mobile/src/lib/app-context.tsx

View workflow job for this annotation

GitHub Actions / Mobile (lint, typecheck, test)

'@typescript-eslint/no-unused-vars' rule is disabled but never reported
const [_, setRefreshKey] = useState(0);

const syncStepFromMockState = useCallback(async (userId?: string) => {
Expand Down Expand Up @@ -229,7 +230,7 @@
}, [ensureAuthenticatedProfile, syncStepFromMockState]);

useEffect(() => {
void login().catch(error => {

Check warning on line 233 in mobile/src/lib/app-context.tsx

View workflow job for this annotation

GitHub Actions / Mobile (lint, typecheck, test)

Expected 'undefined' and instead saw 'void'
console.error('Failed to initialize auth session:', error);
setProfile(null);
setStep('login');
Expand All @@ -238,9 +239,19 @@
}, [login]);

const logout = useCallback(() => {
void supabase.auth.signOut().catch(error => {

Check warning on line 242 in mobile/src/lib/app-context.tsx

View workflow job for this annotation

GitHub Actions / Mobile (lint, typecheck, test)

Expected 'undefined' and instead saw 'void'
console.warn('Failed to sign out from Supabase:', error);
});
void SharedLockState.save({

Check warning on line 245 in mobile/src/lib/app-context.tsx

View workflow job for this annotation

GitHub Actions / Mobile (lint, typecheck, test)

Expected 'undefined' and instead saw 'void'
syncEnabled: false,
dailyLimitSeconds: 0,
todayUsageSeconds: 0,
isBlocked: false,
localDate: mockStore.getMockLocalDate(),
updatedAt: new Date().toISOString(),
}).catch(error => {
console.warn('Failed to clear shared lock state on logout:', error);
});
mockStore.logout();
setProfile(null);
setHasPermission(false);
Expand Down Expand Up @@ -382,6 +393,19 @@
);
mockStore.syncShieldState(newContract.id);
mockStore.checkContractExpiry();
void SharedLockState.save({

Check warning on line 396 in mobile/src/lib/app-context.tsx

View workflow job for this annotation

GitHub Actions / Mobile (lint, typecheck, test)

Expected 'undefined' and instead saw 'void'
syncEnabled: true,
dailyLimitSeconds: newContract.dailyLimitSeconds,
todayUsageSeconds: mockStore.getTodayTotalUsage(),
isBlocked: false,
localDate: mockStore.getMockLocalDate(),
updatedAt: new Date().toISOString(),
}).catch(error => {
console.warn(
'Failed to sync shared lock state after contract creation:',
error,
);
});

setActiveContract(mockStore.getActiveContract());
setRefreshKey(k => k + 1);
Expand All @@ -401,6 +425,24 @@
const refreshContract = useCallback(() => {
mockStore.checkContractExpiry();
const contract = mockStore.getActiveContract();
const usageSeconds = contract ? mockStore.getTodayTotalUsage() : 0;
const isBlocked = contract
? Boolean(
mockStore.getTodayViolation(contract.id) ||
usageSeconds >= contract.dailyLimitSeconds ||
mockStore.isShieldActive(),
)
: false;
void SharedLockState.save({

Check warning on line 436 in mobile/src/lib/app-context.tsx

View workflow job for this annotation

GitHub Actions / Mobile (lint, typecheck, test)

Expected 'undefined' and instead saw 'void'
syncEnabled: Boolean(contract),
dailyLimitSeconds: contract?.dailyLimitSeconds ?? 0,
todayUsageSeconds: usageSeconds,
isBlocked,
localDate: mockStore.getMockLocalDate(),
updatedAt: new Date().toISOString(),
}).catch(error => {
console.warn('Failed to sync shared lock state:', error);
});
setActiveContract(contract);
setRefreshKey(k => k + 1);
}, []);
Expand Down Expand Up @@ -475,7 +517,7 @@
mockStore.simulateUsage(bundleId, seconds);
const contract = mockStore.getActiveContract();
if (contract) {
void notifyIfThresholdReached({

Check warning on line 520 in mobile/src/lib/app-context.tsx

View workflow job for this annotation

GitHub Actions / Mobile (lint, typecheck, test)

Expected 'undefined' and instead saw 'void'
contractId: contract.id,
localDate: mockStore.getMockLocalDate(),
usageSeconds: mockStore.getTodayTotalUsage(),
Expand Down
27 changes: 27 additions & 0 deletions mobile/src/native/shared-lock-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NativeModules } from 'react-native';

type LockStatePayload = {
syncEnabled: boolean;
dailyLimitSeconds: number;
todayUsageSeconds: number;
isBlocked: boolean;
localDate: string;
updatedAt: string;
};

type SharedLockStateModule = {
saveLockState(payload: LockStatePayload): Promise<void>;
};

const module = NativeModules.SharedLockStateModule as
| SharedLockStateModule
| undefined;

export const SharedLockState = {
async save(payload: LockStatePayload): Promise<void> {
if (!module) {
return;
}
await module.saveLockState(payload);
},
};
Loading
Loading