BTHLABS-58: Share Extension in Apple Apps

This commit is contained in:
Tomek Wójcik 2025-10-04 08:02:13 +02:00
parent 0c12f52569
commit 99e9226338
122 changed files with 5488 additions and 411 deletions

View File

@ -7,11 +7,33 @@
objects = {
/* Begin PBXBuildFile section */
4C1159202E8B055F003B34AD /* Save to HotPocket.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4CBCEA4F2E81CB9500722009 /* Save to HotPocket.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4C2F0C692E851BBD0033F5C2 /* Save to HotPocket.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4C70F30D2E8869FB00320048 /* HPShareExtensionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C70F30C2E8869FB00320048 /* HPShareExtensionHelper.m */; };
4C70F30E2E8869FB00320048 /* HPShareExtensionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C70F30C2E8869FB00320048 /* HPShareExtensionHelper.m */; };
4C70F3152E886A8F00320048 /* HPSharedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C70F3142E886A8F00320048 /* HPSharedItem.m */; };
4C70F3162E886A8F00320048 /* HPSharedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C70F3142E886A8F00320048 /* HPSharedItem.m */; };
4C70F3192E886ADD00320048 /* HPSharedItemsContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C70F3182E886ADD00320048 /* HPSharedItemsContainer.m */; };
4C70F31A2E886ADD00320048 /* HPSharedItemsContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C70F3182E886ADD00320048 /* HPSharedItemsContainer.m */; };
4CABCAD62E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4CABCAE02E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
4C1159212E8B055F003B34AD /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4CABCA922E56F0C800D8A354 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 4CBCEA4E2E81CB9500722009;
remoteInfo = "macOS (Share Extension)";
};
4C2F0C672E851BBD0033F5C2 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4CABCA922E56F0C800D8A354 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 4C2F0C5D2E851BBD0033F5C2;
remoteInfo = "iOS (Share Extension)";
};
4CABCAD72E56F0C900D8A354 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4CABCA922E56F0C800D8A354 /* Project object */;
@ -36,6 +58,7 @@
dstSubfolderSpec = 13;
files = (
4CABCAD62E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */,
4C2F0C692E851BBD0033F5C2 /* Save to HotPocket.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
@ -46,6 +69,7 @@
dstPath = "";
dstSubfolderSpec = 13;
files = (
4C1159202E8B055F003B34AD /* Save to HotPocket.appex in Embed Foundation Extensions */,
4CABCAE02E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
@ -54,22 +78,64 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Save to HotPocket.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
4C70F30B2E8869FB00320048 /* HPShareExtensionHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HPShareExtensionHelper.h; sourceTree = "<group>"; };
4C70F30C2E8869FB00320048 /* HPShareExtensionHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HPShareExtensionHelper.m; sourceTree = "<group>"; };
4C70F3132E886A8F00320048 /* HPSharedItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HPSharedItem.h; sourceTree = "<group>"; };
4C70F3142E886A8F00320048 /* HPSharedItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HPSharedItem.m; sourceTree = "<group>"; };
4C70F3172E886ADD00320048 /* HPSharedItemsContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HPSharedItemsContainer.h; sourceTree = "<group>"; };
4C70F3182E886ADD00320048 /* HPSharedItemsContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HPSharedItemsContainer.m; sourceTree = "<group>"; };
4CABCAB02E56F0C900D8A354 /* HotPocket.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HotPocket.app; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCAC62E56F0C900D8A354 /* HotPocket.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HotPocket.app; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "HotPocket Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "HotPocket Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
4CBCEA4F2E81CB9500722009 /* Save to HotPocket.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Save to HotPocket.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
4C2F0C6D2E851BBD0033F5C2 /* Exceptions for "iOS (Share Extension)" folder in "iOS (Share Extension)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 4C2F0C5D2E851BBD0033F5C2 /* iOS (Share Extension) */;
};
4C2F0C6F2E851BF90033F5C2 /* Exceptions for "Shared (App)" folder in "iOS (Share Extension)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Assets.xcassets,
HPAPI.m,
HPCredentialsHelper.m,
HPRPCClient.m,
"NSURL+HotPocketExtensions.m",
"Resources/icon-mac-384.png",
);
target = 4C2F0C5D2E851BBD0033F5C2 /* iOS (Share Extension) */;
};
4C3B958C2E83C83A00F4F82C /* Exceptions for "macOS (App)" folder in "HotPocket (macOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 4CABCAC52E56F0C900D8A354 /* HotPocket (macOS) */;
};
4C7A01792E867D6200DEA460 /* Exceptions for "iOS (App)" folder in "iOS (Share Extension)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
MultilineLabel.m,
);
target = 4C2F0C5D2E851BBD0033F5C2 /* iOS (Share Extension) */;
};
4CABCB042E56F0C900D8A354 /* Exceptions for "Shared (App)" folder in "HotPocket (iOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
"/Localized: Resources/Main.html",
Assets.xcassets,
HPAPI.m,
HPAuthFlow.m,
HPCredentialsHelper.m,
HPRPCClient.m,
"NSURL+HotPocketExtensions.m",
"Resources/icon-mac-384.png",
Resources/Script.js,
Resources/Style.css,
ViewController.m,
);
target = 4CABCAAF2E56F0C900D8A354 /* HotPocket (iOS) */;
};
@ -90,12 +156,13 @@
4CABCB0E2E56F0C900D8A354 /* Exceptions for "Shared (App)" folder in "HotPocket (macOS)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
"/Localized: Resources/Main.html",
Assets.xcassets,
HPAPI.m,
HPAuthFlow.m,
HPCredentialsHelper.m,
HPRPCClient.m,
"NSURL+HotPocketExtensions.m",
"Resources/icon-mac-384.png",
Resources/Script.js,
Resources/Style.css,
ViewController.m,
);
target = 4CABCAC52E56F0C900D8A354 /* HotPocket (macOS) */;
};
@ -134,14 +201,43 @@
);
target = 4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */;
};
4CBCEA612E81CB9500722009 /* Exceptions for "macOS (Share Extension)" folder in "macOS (Share Extension)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 4CBCEA4E2E81CB9500722009 /* macOS (Share Extension) */;
};
4CBCEA632E81CBC800722009 /* Exceptions for "Shared (App)" folder in "macOS (Share Extension)" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Assets.xcassets,
HPAPI.m,
HPCredentialsHelper.m,
HPRPCClient.m,
"NSURL+HotPocketExtensions.m",
"Resources/icon-mac-384.png",
);
target = 4CBCEA4E2E81CB9500722009 /* macOS (Share Extension) */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
4C2F0C5F2E851BBD0033F5C2 /* iOS (Share Extension) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4C2F0C6D2E851BBD0033F5C2 /* Exceptions for "iOS (Share Extension)" folder in "iOS (Share Extension)" target */,
);
path = "iOS (Share Extension)";
sourceTree = "<group>";
};
4CABCA962E56F0C800D8A354 /* Shared (App) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4CABCB042E56F0C900D8A354 /* Exceptions for "Shared (App)" folder in "HotPocket (iOS)" target */,
4CABCB0E2E56F0C900D8A354 /* Exceptions for "Shared (App)" folder in "HotPocket (macOS)" target */,
4CBCEA632E81CBC800722009 /* Exceptions for "Shared (App)" folder in "macOS (Share Extension)" target */,
4C2F0C6F2E851BF90033F5C2 /* Exceptions for "Shared (App)" folder in "iOS (Share Extension)" target */,
);
path = "Shared (App)";
sourceTree = "<group>";
@ -163,12 +259,16 @@
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4CABCB0D2E56F0C900D8A354 /* Exceptions for "iOS (App)" folder in "HotPocket (iOS)" target */,
4C7A01792E867D6200DEA460 /* Exceptions for "iOS (App)" folder in "iOS (Share Extension)" target */,
);
path = "iOS (App)";
sourceTree = "<group>";
};
4CABCAC72E56F0C900D8A354 /* macOS (App) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4C3B958C2E83C83A00F4F82C /* Exceptions for "macOS (App)" folder in "HotPocket (macOS)" target */,
);
path = "macOS (App)";
sourceTree = "<group>";
};
@ -188,9 +288,24 @@
path = "macOS (Extension)";
sourceTree = "<group>";
};
4CBCEA502E81CB9500722009 /* macOS (Share Extension) */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4CBCEA612E81CB9500722009 /* Exceptions for "macOS (Share Extension)" folder in "macOS (Share Extension)" target */,
);
path = "macOS (Share Extension)";
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
4C2F0C5B2E851BBD0033F5C2 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCAAD2E56F0C900D8A354 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -219,18 +334,49 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
4CBCEA4C2E81CB9500722009 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
4C11591F2E8B055F003B34AD /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
4C70F30A2E8869D200320048 /* Shared (Share Extension) */ = {
isa = PBXGroup;
children = (
4C70F30B2E8869FB00320048 /* HPShareExtensionHelper.h */,
4C70F30C2E8869FB00320048 /* HPShareExtensionHelper.m */,
4C70F3132E886A8F00320048 /* HPSharedItem.h */,
4C70F3142E886A8F00320048 /* HPSharedItem.m */,
4C70F3172E886ADD00320048 /* HPSharedItemsContainer.h */,
4C70F3182E886ADD00320048 /* HPSharedItemsContainer.m */,
);
path = "Shared (Share Extension)";
sourceTree = "<group>";
};
4CABCA912E56F0C800D8A354 = {
isa = PBXGroup;
children = (
4CABCA962E56F0C800D8A354 /* Shared (App) */,
4CABCAA02E56F0C900D8A354 /* Shared (Extension) */,
4C70F30A2E8869D200320048 /* Shared (Share Extension) */,
4CABCAB22E56F0C900D8A354 /* iOS (App) */,
4CABCAC72E56F0C900D8A354 /* macOS (App) */,
4CABCAD92E56F0C900D8A354 /* iOS (Extension) */,
4CABCAE32E56F0C900D8A354 /* macOS (Extension) */,
4CBCEA502E81CB9500722009 /* macOS (Share Extension) */,
4C2F0C5F2E851BBD0033F5C2 /* iOS (Share Extension) */,
4C11591F2E8B055F003B34AD /* Frameworks */,
4CABCAB12E56F0C900D8A354 /* Products */,
);
sourceTree = "<group>";
@ -242,6 +388,8 @@
4CABCAC62E56F0C900D8A354 /* HotPocket.app */,
4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */,
4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */,
4CBCEA4F2E81CB9500722009 /* Save to HotPocket.appex */,
4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket.appex */,
);
name = Products;
sourceTree = "<group>";
@ -249,6 +397,28 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
4C2F0C5D2E851BBD0033F5C2 /* iOS (Share Extension) */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4C2F0C6A2E851BBD0033F5C2 /* Build configuration list for PBXNativeTarget "iOS (Share Extension)" */;
buildPhases = (
4C2F0C5A2E851BBD0033F5C2 /* Sources */,
4C2F0C5B2E851BBD0033F5C2 /* Frameworks */,
4C2F0C5C2E851BBD0033F5C2 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
4C2F0C5F2E851BBD0033F5C2 /* iOS (Share Extension) */,
);
name = "iOS (Share Extension)";
packageProductDependencies = (
);
productName = "iOS (Share Extension)";
productReference = 4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket.appex */;
productType = "com.apple.product-type.app-extension";
};
4CABCAAF2E56F0C900D8A354 /* HotPocket (iOS) */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4CABCB0A2E56F0C900D8A354 /* Build configuration list for PBXNativeTarget "HotPocket (iOS)" */;
@ -262,6 +432,7 @@
);
dependencies = (
4CABCAD82E56F0C900D8A354 /* PBXTargetDependency */,
4C2F0C682E851BBD0033F5C2 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
4CABCAB22E56F0C900D8A354 /* iOS (App) */,
@ -286,6 +457,7 @@
);
dependencies = (
4CABCAE22E56F0C900D8A354 /* PBXTargetDependency */,
4C1159222E8B055F003B34AD /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
4CABCAC72E56F0C900D8A354 /* macOS (App) */,
@ -341,6 +513,28 @@
productReference = 4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
4CBCEA4E2E81CB9500722009 /* macOS (Share Extension) */ = {
isa = PBXNativeTarget;
buildConfigurationList = 4CBCEA602E81CB9500722009 /* Build configuration list for PBXNativeTarget "macOS (Share Extension)" */;
buildPhases = (
4CBCEA4B2E81CB9500722009 /* Sources */,
4CBCEA4C2E81CB9500722009 /* Frameworks */,
4CBCEA4D2E81CB9500722009 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
4CBCEA502E81CB9500722009 /* macOS (Share Extension) */,
);
name = "macOS (Share Extension)";
packageProductDependencies = (
);
productName = "macOS (Share Extension)";
productReference = 4CBCEA4F2E81CB9500722009 /* Save to HotPocket.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@ -350,6 +544,9 @@
BuildIndependentTargetsInParallel = 1;
LastUpgradeCheck = 1640;
TargetAttributes = {
4C2F0C5D2E851BBD0033F5C2 = {
CreatedOnToolsVersion = 26.0;
};
4CABCAAF2E56F0C900D8A354 = {
CreatedOnToolsVersion = 16.4;
};
@ -362,6 +559,9 @@
4CABCADE2E56F0C900D8A354 = {
CreatedOnToolsVersion = 16.4;
};
4CBCEA4E2E81CB9500722009 = {
CreatedOnToolsVersion = 16.4;
};
};
};
buildConfigurationList = 4CABCA952E56F0C800D8A354 /* Build configuration list for PBXProject "HotPocket" */;
@ -382,11 +582,20 @@
4CABCAC52E56F0C900D8A354 /* HotPocket (macOS) */,
4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */,
4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */,
4CBCEA4E2E81CB9500722009 /* macOS (Share Extension) */,
4C2F0C5D2E851BBD0033F5C2 /* iOS (Share Extension) */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
4C2F0C5C2E851BBD0033F5C2 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCAAE2E56F0C900D8A354 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -415,9 +624,26 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
4CBCEA4D2E81CB9500722009 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
4C2F0C5A2E851BBD0033F5C2 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4C70F30E2E8869FB00320048 /* HPShareExtensionHelper.m in Sources */,
4C70F3152E886A8F00320048 /* HPSharedItem.m in Sources */,
4C70F3192E886ADD00320048 /* HPSharedItemsContainer.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
4CABCAAC2E56F0C900D8A354 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -446,9 +672,29 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
4CBCEA4B2E81CB9500722009 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4C70F3162E886A8F00320048 /* HPSharedItem.m in Sources */,
4C70F30D2E8869FB00320048 /* HPShareExtensionHelper.m in Sources */,
4C70F31A2E886ADD00320048 /* HPSharedItemsContainer.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
4C1159222E8B055F003B34AD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4CBCEA4E2E81CB9500722009 /* macOS (Share Extension) */;
targetProxy = 4C1159212E8B055F003B34AD /* PBXContainerItemProxy */;
};
4C2F0C682E851BBD0033F5C2 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4C2F0C5D2E851BBD0033F5C2 /* iOS (Share Extension) */;
targetProxy = 4C2F0C672E851BBD0033F5C2 /* PBXContainerItemProxy */;
};
4CABCAD82E56F0C900D8A354 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */;
@ -462,6 +708,71 @@
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
4C2F0C6B2E851BBD0033F5C2 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025092901;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Save to HotPocket";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 25.9.17;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
4C2F0C6C2E851BBD0033F5C2 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Save to HotPocket";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 25.9.17;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
4CABCB062E56F0C900D8A354 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -487,6 +798,10 @@
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
@ -517,6 +832,10 @@
PRODUCT_NAME = "HotPocket Extension";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
@ -528,6 +847,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
@ -539,7 +859,8 @@
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -569,6 +890,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
@ -580,7 +902,8 @@
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -610,7 +933,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
@ -625,7 +948,7 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
@ -633,6 +956,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
PRODUCT_NAME = "HotPocket Extension";
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -658,7 +982,7 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.9.17;
OTHER_LDFLAGS = (
"-framework",
@ -666,6 +990,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.Extension;
PRODUCT_NAME = "HotPocket Extension";
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -678,12 +1003,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "macOS (App)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
@ -721,6 +1047,7 @@
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "macOS (App)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = HotPocket;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
@ -859,9 +1186,76 @@
};
name = Release;
};
4CBCEA5E2E81CB9500722009 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "macOS (Share Extension)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Save to HotPocket";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.9.17;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket";
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
};
name = Debug;
};
4CBCEA5F2E81CB9500722009 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2025091701;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "macOS (Share Extension)/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Save to HotPocket";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-present BTHLabs";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.9.17;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket";
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
4C2F0C6A2E851BBD0033F5C2 /* Build configuration list for PBXNativeTarget "iOS (Share Extension)" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4C2F0C6B2E851BBD0033F5C2 /* Debug */,
4C2F0C6C2E851BBD0033F5C2 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4CABCA952E56F0C800D8A354 /* Build configuration list for PBXProject "HotPocket" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@ -907,6 +1301,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
4CBCEA602E81CB9500722009 /* Build configuration list for PBXNativeTarget "macOS (Share Extension)" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4CBCEA5E2E81CB9500722009 /* Debug */,
4CBCEA5F2E81CB9500722009 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 4CABCA922E56F0C800D8A354 /* Project object */;

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAAF2E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (iOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAAF2E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (iOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAAF2E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (iOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAC52E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAC52E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAC52E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAD42E56F0C900D8A354"
BuildableName = "HotPocket Extension.appex"
BlueprintName = "HotPocket Extension (iOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAAF2E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (iOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAAF2E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (iOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAAF2E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (iOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCADE2E56F0C900D8A354"
BuildableName = "HotPocket Extension.appex"
BlueprintName = "HotPocket Extension (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAC52E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAC52E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAC52E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CBCEA4E2E81CB9500722009"
BuildableName = "Save to HotPocket.appex"
BlueprintName = "macOS (Share Extension)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAC52E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAC52E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4CABCAC52E56F0C900D8A354"
BuildableName = "HotPocket.app"
BlueprintName = "HotPocket (macOS)"
ReferencedContainer = "container:HotPocket.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.463",
"green" : "0.392",
"red" : "0.933"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x67",
"green" : "0xA7",
"red" : "0x0E"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2E",
"green" : "0x96",
"red" : "0xF1"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,32 @@
//
// HPAPI.h
// HotPocket
//
// Created by Tomek Wójcik on 23/09/2025.
//
#import <Foundation/Foundation.h>
#import "HPRPCClient.h"
NS_ASSUME_NONNULL_BEGIN
typedef void (^HPAPICheckAuthCompletionHandler)(BOOL authValid, NSError * _Nullable error, NSString * _Nullable callId);
@interface HPAPI : NSObject
@property (nullable) HPRPCClient *rpcClient;
@property NSMutableSet *callIds;
-(id)initWithRPCClientDelegate:(id<HPRPCClientDelegate>)delegate;
+(NSDictionary *)getAccessTokenMeta;
-(NSString *)checkAuth;
-(void)checkAuth:(HPAPICheckAuthCompletionHandler)completionHandler;
-(NSString *)save:(NSURL *)url;
-(BOOL)save:(NSURL *)url completionHandler:(HPRPCClientCompletionHandler)completionHandler;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,148 @@
//
// HPAPI.m
// HotPocket
//
// Created by Tomek Wójcik on 23/09/2025.
//
#import "HPAPI.h"
#import "HPCredentialsHelper.h"
#import "HPRPCClient.h"
@implementation HPAPI (HPAPIPrivate)
#pragma mark - Private interface
-(void)updateRPCClientCredentials {
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
self.rpcClient.baseURL = credentials.rpcURL;
self.rpcClient.accessToken = credentials.accessToken;
}
@end
@implementation HPAPI
#pragma mark - Initialization
-(id)init {
if (self = [super init]) {
self.rpcClient = [[HPRPCClient alloc] initWithBaseURL:nil accessToken:nil];
[self updateRPCClientCredentials];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onCredentialsChanged:)
name:@"HPCredentialsChanged"
object:nil];
}
return self;
}
-(id)initWithRPCClientDelegate:(id<HPRPCClientDelegate>)rpcClientDelegate {
if (self = [self init]) {
self.rpcClient.delegate = rpcClientDelegate;
}
return self;
}
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Public interface
+(NSDictionary *)getAccessTokenMeta {
NSString *platform = @"macOS";
#ifdef TARGET_OS_IOS
platform = @"iPhone";
#endif
return @{
@"version": [[[NSBundle mainBundle] infoDictionary] valueForKey:@"CFBundleShortVersionString"],
@"platform": platform,
};
}
-(NSString *)checkAuth {
if (self.rpcClient.hasCredentials == NO) {
return nil;
}
NSString *callId = [[NSUUID UUID] UUIDString];
BOOL callResult = [self.rpcClient call:callId
method:@"accounts.auth.check_access_token"
params:@[self.rpcClient.accessToken, [HPAPI getAccessTokenMeta]]
endopoint:@"/accounts/rpc/"];
if (callResult == NO) {
return nil;
}
return callId;
}
-(void)checkAuth:(HPAPICheckAuthCompletionHandler)completionHandler {
if (self.rpcClient.hasCredentials == NO) {
completionHandler(NO, nil, nil);
} else {
NSString *callId = [[NSUUID UUID] UUIDString];
BOOL callResult = [self.rpcClient call:callId
method:@"accounts.auth.check_access_token"
params:@[self.rpcClient.accessToken, [HPAPI getAccessTokenMeta]]
endopoint:@"/accounts/rpc/" completionHandler:^(NSString *finalCallId, HPRPCCallResult *result) {
BOOL authValid = YES;
if (result.error != nil) {
authValid = NO;
} else if ([(NSNumber *)result.result boolValue] == NO) {
authValid = NO;
}
completionHandler(authValid, result.error, finalCallId);
}];
}
}
-(NSString *)save:(NSURL *)url {
if (self.rpcClient.hasCredentials == NO) {
return nil;
}
if (url == nil) {
return nil;
}
NSString *callId = [[NSUUID UUID] UUIDString];
BOOL callResult = [self.rpcClient call:callId method:@"saves.create" params:@[[url absoluteString]]];
if (callResult == NO) {
return nil;
}
return callId;
}
-(BOOL)save:(NSURL *)url completionHandler:(HPRPCClientCompletionHandler)completionHandler {
if (self.rpcClient.hasCredentials == NO) {
return NO;
}
if (url == nil) {
return NO;
}
NSString *callId = [[NSUUID UUID] UUIDString];
return [self.rpcClient call:callId
method:@"saves.create"
params:@[[url absoluteString]]
completionHandler:completionHandler];
}
#pragma mark - Notification handlers
-(void)onCredentialsChanged:(NSNotification *)notification {
[self updateRPCClientCredentials];
}
@end

View File

@ -0,0 +1,30 @@
//
// HPAuthFlow.h
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 21/09/2025.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HPAuthParams : NSObject
@property (copy) NSString *authKey;
@property (copy) NSString *sessionToken;
@end
@interface HPAuthFlow : NSObject
@property (nullable) NSURL *baseURL;
@property (nullable) NSString *sessionToken;
-(NSURL *)start;
-(HPAuthParams *)handlePostAuthenticateURL:(NSURL *)url;
-(BOOL)handleAuthParams:(HPAuthParams *)authParams;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,140 @@
//
// HPAuthFlow.m
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 21/09/2025.
//
#import "HPAuthFlow.h"
#import "HPAPI.h"
#import "HPCredentialsHelper.h"
#import "HPRPCClient.h"
@implementation HPAuthParams
#pragma mark - HPAuthParams implementation
@end
@implementation HPAuthFlow (HPAuthFlowPrivate)
#pragma mark - HPAuthFlow private interface
-(NSURL *)resolveAuthenticateURL {
if (self.baseURL == nil) {
return nil;
}
NSURL *authURL = [self.baseURL URLByAppendingPathComponent:@"/integrations/extension/authenticate/"];
if (authURL.scheme == nil) {
return nil;
}
NSBundle *mainBundle = [NSBundle mainBundle];
NSURLComponents *authURLComponents = [NSURLComponents componentsWithURL:authURL resolvingAgainstBaseURL:NO];
authURLComponents.queryItems = @[
[NSURLQueryItem queryItemWithName:@"source" value:[[mainBundle infoDictionary] valueForKey:@"HPAuthFlowSource"]],
[NSURLQueryItem queryItemWithName:@"session_token" value:self.sessionToken],
];
return authURLComponents.URL;
}
@end
@implementation HPAuthFlow
#pragma mark - Initialization
-(id)init {
if (self = [super init]) {
self.baseURL = nil;
self.sessionToken = nil;
}
return self;
}
#pragma mark - Public interface
-(NSURL *)start {
if (self.baseURL == nil) {
return nil;
}
if (self.sessionToken == nil) {
self.sessionToken = [[NSUUID UUID] UUIDString];
}
return [self resolveAuthenticateURL];
}
-(HPAuthParams *)handlePostAuthenticateURL:(NSURL *)url {
if (url == nil) {
return nil;
}
NSDictionary *postAuthenticateURLParams = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"HPAuthFlowPostAuthenticateURLParts"];
if (postAuthenticateURLParams == nil) {
return nil;
}
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
if ([urlComponents.scheme isEqualToString:[postAuthenticateURLParams valueForKey:@"scheme"]] == NO) {
return nil;
}
if ([urlComponents.host isEqualToString:[postAuthenticateURLParams valueForKey:@"host"]] == NO) {
return nil;
}
HPAuthParams *result = [[HPAuthParams alloc] init];
for (NSURLQueryItem *queryItem in urlComponents.queryItems) {
if ([queryItem.name isEqualToString:@"auth_key"] == YES) {
result.authKey = queryItem.value;
} else if ([queryItem.name isEqualToString:@"session_token"] == YES) {
result.sessionToken = queryItem.value;
}
}
if ([self.sessionToken isEqualToString:result.sessionToken] == NO) {
return nil;
}
return result;
}
-(BOOL)handleAuthParams:(HPAuthParams *)authParams {
HPRPCClient *rpcClient = [[HPRPCClient alloc] initWithBaseURL:self.baseURL accessToken:nil];
NSArray *callParams = @[
authParams.authKey,
[HPAPI getAccessTokenMeta],
];
BOOL callResult = [rpcClient call:self.sessionToken
method:@"accounts.access_tokens.create"
params:callParams endopoint:@"/accounts/rpc/"
completionHandler:^(NSString *callId, HPRPCCallResult *result) {
dispatch_async(dispatch_get_main_queue(), ^{
if (result.error != nil) {
NSLog(@"-[HPAuthFlow handleAuthParams:] error=`%@`", result.error);
} else {
HPCredentialsHelper *credentialsHelper = [HPCredentialsHelper sharedHelper];
[credentialsHelper saveCredentials:[self.baseURL absoluteString] accessToken:(NSString *)result.result];
}
self.sessionToken = nil;
[[NSNotificationCenter defaultCenter] postNotificationName:@"AuthFlowDidFinish" object:self];
});
}];
return callResult;
}
@end

View File

@ -0,0 +1,32 @@
//
// HPCredentialsHelper.h
// HotPocket
//
// Created by Tomek Wójcik on 19/09/2025.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HPCredentials : NSObject
@property (nullable) NSString *baseURL;
@property (nullable) NSString *accessToken;
@property (readonly) BOOL usable;
@property (readonly) NSURL *rpcURL;
@end
@interface HPCredentialsHelper : NSObject
+(instancetype)sharedHelper;
-(HPCredentials *)getCredentials;
-(BOOL)saveCredentials:(NSString *)baseURL accessToken:(NSString *)accessToken;
-(BOOL)clearCredentials;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,218 @@
//
// HPCredentialsHelper.m
// HotPocket
//
// Created by Tomek Wójcik on 19/09/2025.
//
#import <Security/Security.h>
#import "HPCredentialsHelper.h"
@implementation HPCredentials
#pragma mark - HPCredentials implementation
-(id)init {
if (self = [super init]) {
self.baseURL = nil;
self.accessToken = nil;
}
return self;
}
-(BOOL)usable {
return (self.baseURL != nil && self.accessToken != nil);
}
-(NSURL *)rpcURL {
return [NSURL URLWithString:self.baseURL];
}
-(NSString *)description {
NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithCapacity:2];
if (self.baseURL == nil) {
[attributes setValue:@"(null)" forKey:@"baseURL"];
} else {
[attributes setValue:self.baseURL forKey:@"baseURL"];
}
if (self.accessToken == nil) {
[attributes setValue:@"(null)" forKey:@"accessToken"];
} else {
[attributes setValue:@"***" forKey:@"accessToken"];
}
return [NSString stringWithFormat:@"<%@: %p; %@>", NSStringFromClass([self class]), self, attributes];
}
@end
@implementation HPCredentialsHelper (HPCredentialsHelperPrivate)
#pragma mark - Private interface
-(NSString *)getService {
#ifdef DEBUG
return @"pl.bthlabs.HotPocket.Debug";
#else
return @"pl.bthlabs.HotPocket";
#endif
}
-(NSData *)getKeychainItem:(NSString *)service account:(NSString *)account {
if (service == nil || account == nil) {
return nil;
}
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: account,
(__bridge id)kSecReturnData: @YES,
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne,
(__bridge id)kSecAttrAccessGroup: @"648728X64K.pl.bthlabs.HotPocketShared",
(__bridge id)kSecUseDataProtectionKeychain: @YES,
};
CFTypeRef resultData = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &resultData);
if (status == errSecSuccess && resultData != NULL) {
NSData *result = (__bridge_transfer NSData *)resultData;
return result;
} else {
CFStringRef statusStringRef = SecCopyErrorMessageString(status, NULL);
NSString *statusString = (__bridge NSString *)statusStringRef;
NSLog(@"-[HPCredentialsHelper getKeychainItem:account:] service=`%@` account=`%@` status=%@", service, account, statusString);
return nil;
}
}
-(BOOL)createKeychainItemWithValue:(NSData *)value service:(NSString *)service account:(NSString *)account {
if (value == nil || service == nil || account == nil) {
return NO;
}
NSDictionary *attributes = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: account,
(__bridge id)kSecValueData: value,
(__bridge id)kSecAttrAccessGroup: @"648728X64K.pl.bthlabs.HotPocketShared",
(__bridge id)kSecUseDataProtectionKeychain: @YES,
};
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, NULL);
if (status != errSecSuccess) {
CFStringRef statusStringRef = SecCopyErrorMessageString(status, NULL);
NSString *statusString = (__bridge NSString *)statusStringRef;
NSLog(@"-[HPCredentialsHelper createKeychainItemWithValue:service:account:] service=`%@` account=`%@` status=%@", service, account, statusString);
return NO;
}
return YES;
}
-(BOOL)deleteKeychainItem:(NSString *)service account:(NSString *)account {
if (service == nil || account == nil) {
return NO;
}
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: service,
(__bridge id)kSecAttrAccount: account,
(__bridge id)kSecAttrAccessGroup: @"648728X64K.pl.bthlabs.HotPocketShared",
(__bridge id)kSecUseDataProtectionKeychain: @YES,
};
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
if (status != errSecSuccess) {
CFStringRef statusStringRef = SecCopyErrorMessageString(status, NULL);
NSString *statusString = (__bridge NSString *)statusStringRef;
NSLog(@"-[HPCredentialsHelper deleteKeychainItem:account:] service=`%@` account=`%@` status=%@", service, account, statusString);
return NO;
}
return YES;
}
@end
@implementation HPCredentialsHelper
#pragma mark - Initialization
+(instancetype)sharedHelper {
static HPCredentialsHelper *sharedInstance = nil;
static dispatch_once_t initToken;
dispatch_once(&initToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
#pragma mark - Public interface
-(HPCredentials *)getCredentials {
HPCredentials *result = [[HPCredentials alloc] init];
NSData *itemData = [self getKeychainItem:[self getService] account:@"RPC"];
if (itemData != nil) {
NSError *error;
NSDictionary *itemPayload = [NSJSONSerialization JSONObjectWithData:itemData
options:NSJSONReadingTopLevelDictionaryAssumed
error:&error];
if (error != nil) {
NSLog(@"-[HPCredentialsHalper getCredentials] error=`%@`", error);
} else if (itemPayload != nil) {
result.baseURL = [itemPayload valueForKey:@"baseURL"];
result.accessToken = [itemPayload valueForKey:@"accessToken"];
}
}
return result;
}
-(BOOL)saveCredentials:(NSString *)baseURL accessToken:(NSString *)accessToken {
NSMutableDictionary *itemPayload = [NSMutableDictionary dictionaryWithCapacity:2];
if (baseURL != nil) {
[itemPayload setValue:baseURL forKey:@"baseURL"];
}
if (accessToken != nil) {
[itemPayload setValue:accessToken forKey:@"accessToken"];
}
NSError *error;
NSData *itemData = [NSJSONSerialization dataWithJSONObject:itemPayload options:0 error:&error];
if (error != nil) {
NSLog(@"-[HPCredentialsHalper saveCredentials:accessToken:] error=`%@`", error);
return NO;
}
BOOL saveResult = [self createKeychainItemWithValue:itemData service:[self getService] account:@"RPC"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"HPCredentialsChanged" object:self];
return saveResult;
}
-(BOOL)clearCredentials {
BOOL deleteResult = [self deleteKeychainItem:[self getService] account:@"RPC"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"HPCredentialsChanged" object:self];
return deleteResult;
}
@end

View File

@ -0,0 +1,44 @@
//
// HPRPCClient.h
// HotPocket
//
// Created by Tomek Wójcik on 19/09/2025.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HPRPCCallResult : NSObject
@property (nullable) NSError *error;
@property (nullable) id result;
@end
@protocol HPRPCClientDelegate <NSObject>
-(void)rpcClientDidReceiveResult:(HPRPCCallResult *)result callId:(NSString *)callId;
@end
typedef void (^HPRPCClientCompletionHandler)(NSString * _Nullable callId, HPRPCCallResult * _Nullable result);
@interface HPRPCClient : NSObject
@property (nonatomic, weak) id<HPRPCClientDelegate> delegate;
@property NSURL *baseURL;
@property NSString *accessToken;
@property NSURLSession *session;
-(id)initWithBaseURL:(nullable NSURL *)baseURL accessToken:(nullable NSString *)accessToken;
-(BOOL)hasCredentials;
-(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params endopoint:(nullable NSString *)endpoint completionHandler:(HPRPCClientCompletionHandler)completionHandler;
-(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params endopoint:(nullable NSString *)endpoint;
-(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params;
-(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params completionHandler:(HPRPCClientCompletionHandler)completionHandler;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,185 @@
//
// HPRPCClient.m
// HotPocket
//
// Created by Tomek Wójcik on 19/09/2025.
//
#import "HPRPCClient.h"
@implementation HPRPCCallResult
#pragma mark - HPRPCCallResult implementation
-(id)init {
if (self = [super init]) {
self.error = nil;
self.result = nil;
}
return self;
}
-(NSString *)description {
NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithCapacity:2];
if (self.error == nil) {
[attributes setValue:@"(null)" forKey:@"error"];
} else {
[attributes setValue:self.error forKey:@"error"];
}
if (self.result == nil) {
[attributes setValue:@"(null)" forKey:@"result"];
} else {
[attributes setValue:self.result forKey:@"result"];
}
return [NSString stringWithFormat:@"<%@: %p; %@>", NSStringFromClass([self class]), self, attributes];
}
@end
@implementation HPRPCClient
#pragma mark - Initialization
-(id)initWithBaseURL:(nullable NSURL *)baseURL accessToken:(nullable NSString *)accessToken {
if (self = [super init]) {
self.baseURL = baseURL;
self.accessToken = accessToken;
self.session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.ephemeralSessionConfiguration];
}
return self;
}
#pragma mark - Public interface
-(BOOL)hasCredentials {
return (self.baseURL != nil && self.accessToken != nil);
}
-(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params endopoint:(nullable NSString *)endpoint completionHandler:(HPRPCClientCompletionHandler)completionHandler {
if (self.baseURL == nil) {
return NO;
}
if (callId == nil) {
callId = [[NSUUID UUID] UUIDString];
}
if (endpoint == nil) {
endpoint = @"/rpc/";
}
NSBundle *mainBundle = [NSBundle mainBundle];
NSDictionary *payload = @{
@"jsonrpc": @"2.0",
@"id": callId,
@"method": method,
@"params": params,
};
NSError *error;
NSData *jsonPayload = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
if (!jsonPayload) {
NSLog(@"-[HPRPCClient call:method:params:endpoint:] Unable to serialize payload: error=`%@`", error);
HPRPCCallResult *result = [[HPRPCCallResult alloc] init];
result.error = error;
if (self.delegate != nil) {
[self.delegate rpcClientDidReceiveResult:result callId:callId];
}
return NO;
}
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:[self.baseURL URLByAppendingPathComponent:endpoint] resolvingAgainstBaseURL:NO];
urlComponents.queryItems = @[
[NSURLQueryItem queryItemWithName:@"method" value:method],
];
NSURL *callURL = [urlComponents URL];
#ifdef DEBUG
NSLog(@"-[HPRPCClient call:method:params:endpoint:] callURL=`%@`", callURL.absoluteString);
#endif
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:callURL];
request.HTTPMethod = @"POST";
request.HTTPBody = jsonPayload;
[request setValue:@"application/json;charset=utf-8" forHTTPHeaderField:@"Content-Type"];
[request setValue:[[mainBundle infoDictionary] valueForKey:@"HPRPCClientOrigin"] forHTTPHeaderField:@"Origin"];
if (self.accessToken != nil) {
NSString *authorization = [NSString stringWithFormat:@"Bearer %@", self.accessToken];
[request setValue:authorization forHTTPHeaderField:@"Authorization"];
}
NSString *build = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
NSString *userAgent = [NSString stringWithFormat:@"HotPocket/%@", build];
[request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
HPRPCCallResult *result = [[HPRPCCallResult alloc] init];
if (error != nil) {
result.error = error;
} else {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode != 200) {
result.error = [[NSError alloc] initWithDomain:@"pl.bthlabs.HotPocket.HPRPCClient" code:-32000 userInfo:@{
@"callId": callId,
@"url": httpResponse.URL,
@"statusCode": [NSNumber numberWithInteger:httpResponse.statusCode],
@"response": response,
}];
} else {
NSError *jsonDecodeError;
NSDictionary *callResult = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingTopLevelDictionaryAssumed error:&error];
if (jsonDecodeError != nil) {
result.error = jsonDecodeError;
} else {
NSDictionary *rpcError = [callResult valueForKey:@"error"];
if (rpcError != nil) {
NSNumber *rpcErrorCode = [rpcError valueForKey:@"code"];
if (rpcErrorCode == nil) {
rpcErrorCode = [NSNumber numberWithInt:-32000];
}
result.error = [[NSError alloc] initWithDomain:@"pl.bthlabs.HotPocket.HPRPCClient" code:[rpcErrorCode integerValue] userInfo:rpcError];
} else {
result.result = [callResult valueForKey:@"result"];
}
}
}
}
if (completionHandler) {
completionHandler(callId, result);
}
}];
[task resume];
return YES;
}
-(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params endopoint:(nullable NSString *)endpoint {
return [self call:callId method:method params:params endopoint:endpoint completionHandler:^(NSString *callId, HPRPCCallResult *result) {
if (self.delegate != nil) {
[self.delegate rpcClientDidReceiveResult:result callId:callId];
}
}];
}
-(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params {
return [self call:callId method:method params:params endopoint:nil];
}
-(BOOL)call:(nullable NSString *)callId method:(NSString *)method params:(NSArray *)params completionHandler:(HPRPCClientCompletionHandler)completionHandler {
return [self call:callId method:method params:params endopoint:nil completionHandler:completionHandler];
}
@end

View File

@ -0,0 +1,18 @@
//
// NSURL+HotPocketExtensions.h
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 30/09/2025.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (HotPocketExtensions)
-(BOOL)isUsableInHotPocket;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,30 @@
//
// NSURL+HotPocketExtensions.m
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 30/09/2025.
//
#import "NSURL+HotPocketExtensions.h"
@implementation NSURL (HotPocketExtensions)
-(BOOL)isUsableInHotPocket {
static NSArray *supportedSchemes = @[@"http", @"https"];
if (self.baseURL != nil) {
return NO;
}
if (self.scheme == nil || [supportedSchemes containsObject:self.scheme] == NO) {
return NO;
}
if (self.host == nil || [@"" isEqualToString:self.host] == YES) {
return NO;
}
return YES;
}
@end

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="stylesheet" href="../Style.css">
<script src="../Script.js" defer></script>
</head>
<body>
<img src="../icon-mac-384.png" width="128" height="128" alt="HotPocket Icon">
<p class="platform-ios">You can turn on Save to Hotpocket Safari extension in Settings.</p>
<p class="platform-mac state-unknown">You can turn on Save to Hotpocket extension in Safari Extensions preferences.</p>
<p class="platform-mac state-on">Save to Hotpocket extension is currently on. You can turn it off in Safari Extensions preferences.</p>
<p class="platform-mac state-off">Save to Hotpocket extension is currently off. You can turn it on in Safari Extensions preferences.</p>
<button class="platform-mac open-preferences">Quit and Open Safari Extensions Preferences…</button>
</body>
</html>

View File

@ -1,24 +0,0 @@
function show(platform, enabled, useSettingsInsteadOfPreferences) {
document.body.classList.add(`platform-${platform}`);
if (useSettingsInsteadOfPreferences) {
document.getElementsByClassName('platform-mac state-on')[0].innerText = "Save to Hotpocket extension is currently on. You can turn it off in the Extensions section of Safari Settings.";
document.getElementsByClassName('platform-mac state-off')[0].innerText = "Save to Hotpocket extension is currently off. You can turn it on in the Extensions section of Safari Settings.";
document.getElementsByClassName('platform-mac state-unknown')[0].innerText = "You can turn on Save to Hotpocket extension in the Extensions section of Safari Settings.";
document.getElementsByClassName('platform-mac open-preferences')[0].innerText = "Quit and Open Safari Settings…";
}
if (typeof enabled === "boolean") {
document.body.classList.toggle(`state-on`, enabled);
document.body.classList.toggle(`state-off`, !enabled);
} else {
document.body.classList.remove(`state-on`);
document.body.classList.remove(`state-off`);
}
}
function openPreferences() {
webkit.messageHandlers.controller.postMessage("open-preferences");
}
document.querySelector("button.open-preferences").addEventListener("click", openPreferences);

View File

@ -1,63 +0,0 @@
* {
-webkit-user-select: none;
-webkit-user-drag: none;
cursor: default;
}
:root {
color-scheme: light dark;
--spacing: 20px;
}
html {
height: 100%;
}
body {
background: #212529;
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--spacing);
margin: 0 calc(var(--spacing) * 2);
height: 100%;
font: -apple-system-short-body;
text-align: center;
}
body:not(.platform-mac, .platform-ios) :is(.platform-mac, .platform-ios) {
display: none;
}
body.platform-ios .platform-mac {
display: none;
}
body.platform-mac .platform-ios {
display: none;
}
body.platform-ios .platform-mac {
display: none;
}
body:not(.state-on, .state-off) :is(.state-on, .state-off) {
display: none;
}
body.state-on :is(.state-off, .state-unknown) {
display: none;
}
body.state-off :is(.state-on, .state-unknown) {
display: none;
}
button {
font-size: 1em;
}

View File

@ -1,26 +0,0 @@
//
// ViewController.h
// Shared (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <TargetConditionals.h>
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
typedef UIViewController PlatformViewController;
#elif TARGET_OS_OSX
#import <Cocoa/Cocoa.h>
typedef NSViewController PlatformViewController;
#endif
@interface ViewController : PlatformViewController
@end

View File

@ -1,76 +0,0 @@
//
// ViewController.m
// Shared (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "ViewController.h"
#import <WebKit/WebKit.h>
#if TARGET_OS_OSX
#import <SafariServices/SafariServices.h>
#endif
static NSString * const extensionBundleIdentifier = @"pl.bthlabs.HotPocket.HotPocket.Extension";
@interface ViewController () <WKNavigationDelegate, WKScriptMessageHandler>
@property (nonatomic) IBOutlet WKWebView *webView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_webView.navigationDelegate = self;
#if TARGET_OS_IOS
_webView.scrollView.scrollEnabled = NO;
#endif
[_webView.configuration.userContentController addScriptMessageHandler:self name:@"controller"];
[_webView loadFileURL:[NSBundle.mainBundle URLForResource:@"Main" withExtension:@"html"] allowingReadAccessToURL:NSBundle.mainBundle.resourceURL];
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
#if TARGET_OS_IOS
[webView evaluateJavaScript:@"show('ios')" completionHandler:nil];
#elif TARGET_OS_OSX
[webView evaluateJavaScript:@"show('mac')" completionHandler:nil];
[SFSafariExtensionManager getStateOfSafariExtensionWithIdentifier:extensionBundleIdentifier completionHandler:^(SFSafariExtensionState *state, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!state) {
// Insert code to inform the user something went wrong.
return;
}
NSString *isExtensionEnabledAsString = state.isEnabled ? @"true" : @"false";
if (@available(macOS 13, *))
[webView evaluateJavaScript:[NSString stringWithFormat:@"show('mac', %@, true)", isExtensionEnabledAsString] completionHandler:nil];
else
[webView evaluateJavaScript:[NSString stringWithFormat:@"show('mac', %@, false)", isExtensionEnabledAsString] completionHandler:nil];
});
}];
#endif
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
#if TARGET_OS_OSX
if (![message.body isEqualToString:@"open-preferences"])
return;
[SFSafariApplication showPreferencesForExtensionWithIdentifier:extensionBundleIdentifier completionHandler:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[NSApp terminate:self];
});
}];
#endif
}
@end

View File

@ -0,0 +1,27 @@
//
// HPShareExtensionHelper.h
// HotPocket
//
// Created by Tomek Wójcik on 27/09/2025.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class HPSharedItemsContainer;
typedef void (^HPShareExtensionHelperHandleItemsCompletionHandler)(NSURL * _Nullable url);
@interface HPShareExtensionHelper : NSObject
@property NSExtensionContext *context;
@property HPSharedItemsContainer *items;
-(instancetype)initWithContext:(NSExtensionContext *)context;
-(void)processItems:(HPShareExtensionHelperHandleItemsCompletionHandler)completionHandler;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,102 @@
//
// HPShareExtensionHelper.m
// HotPocket
//
// Created by Tomek Wójcik on 27/09/2025.
//
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import "HPShareExtensionHelper.h"
#import "HPSharedItem.h"
#import "HPSharedItemsContainer.h"
@implementation HPShareExtensionHelper (HPShareExtensionHelperPrivate)
@end
@implementation HPShareExtensionHelper
-(instancetype)initWithContext:(NSExtensionContext *)context {
if (self = [super init]) {
self.context = context;
self.items = [[HPSharedItemsContainer alloc] init];
}
return self;
}
-(void)processItems:(HPShareExtensionHelperHandleItemsCompletionHandler)completionHandler {
// Depending on the app, the URL might be stored in `public.url` attachment or elsewhere.
// For example, the YouTube app passes it in `public.plain-text`. Because of course it does.
// Furthermore, for some bizarre reason the recommended way of extracting the URL when sharing from a browser
// is to run a JS snippet and examine its output.
// This method will iterate through all the shared items and their attachments and attempt to extract
// the URL candidates.
//
// Also note that handler for `public.url` explicitly requests the payload to be corced to `NSURL *`. Leaving it
// at `NSData *` would cause iOS to, wait for it!, fetch the URL and dump the response body in the payload :D.
//
// This is so _so_ *so* dumb. But hey, at least I learned how to to "chords" in CGD ¯\_()_/¯
UTType *propertyListType = [UTType typeWithFilenameExtension:@"plist"];
dispatch_group_t dispatchGroup = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("HPShareExtensionHelper.processItems.queue", DISPATCH_QUEUE_SERIAL);
for (NSExtensionItem *inputItem in self.context.inputItems) {
#ifdef DEBUG
NSLog(@"-[HPShareExtensionHelper processItems:] inputItem.userInfo=`%@`", inputItem);
#endif
[inputItem.attachments enumerateObjectsUsingBlock:^(NSItemProvider *attachment, NSUInteger index, BOOL *stop) {
dispatch_group_enter(dispatchGroup);
if ([attachment hasItemConformingToTypeIdentifier:propertyListType.identifier] == YES) {
[attachment loadItemForTypeIdentifier:propertyListType.identifier
options:nil
completionHandler:^(NSDictionary *payload, NSError *error) {
dispatch_async(queue, ^{
self.items.primaryItem = [[HPSharedItem alloc] initWithPayload:payload
typeIdentifier:propertyListType.identifier
error:error];
dispatch_group_leave(dispatchGroup);
});
}];
} else if ([attachment hasItemConformingToTypeIdentifier:@"public.url"] == YES) {
[attachment loadItemForTypeIdentifier:@"public.url"
options:nil
completionHandler:^(NSURL *payload, NSError *error) {
dispatch_async(queue, ^{
[self.items.candidateItems addObject:[[HPSharedItem alloc] initWithPayload:payload
typeIdentifier:@"public.url"
error:error]];
dispatch_group_leave(dispatchGroup);
});
}];
} else if ([attachment hasItemConformingToTypeIdentifier:@"public.plain-text"] == YES) {
[attachment loadItemForTypeIdentifier:@"public.plain-text"
options:nil
completionHandler:^(NSString *payload, NSError *error) {
dispatch_async(queue, ^{
[self.items.candidateItems addObject:[[HPSharedItem alloc] initWithPayload:payload
typeIdentifier:@"public.plain-text"
error:error]];
dispatch_group_leave(dispatchGroup);
});
}];
} else {
dispatch_group_leave(dispatchGroup);
}
}];
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{
NSURL *result = [self.items resolveURL];
completionHandler(result);
});
}
}
@end

View File

@ -0,0 +1,24 @@
//
// HPSharedItem.h
// HotPocket
//
// Created by Tomek Wójcik on 27/09/2025.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HPSharedItem : NSObject
@property (nullable) id payload;
@property NSString *typeIdentifier;
@property (nullable) NSError *error;
-(instancetype)initWithPayload:(nullable id)payload typeIdentifier:(NSString *)typeIdentifier error:(nullable NSError *)error;
-(NSURL *)maybeURL;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,47 @@
//
// HPSharedItem.m
// HotPocket
//
// Created by Tomek Wójcik on 27/09/2025.
//
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import "HPSharedItem.h"
@implementation HPSharedItem
-(instancetype)initWithPayload:(id)payload typeIdentifier:(NSString *)typeIdentifier error:(NSError *)error {
if (self = [super init]) {
self.payload = payload;
self.typeIdentifier = typeIdentifier;
self.error = error;
}
return self;
}
-(NSURL *)maybeURL {
if (self.error != nil) {
return nil;
}
if ([self.typeIdentifier isEqualToString:[UTType typeWithFilenameExtension:@"plist"].identifier] == YES) {
NSDictionary *propertyList = self.payload;
NSDictionary *jsHelperResult = [propertyList valueForKey:NSExtensionJavaScriptPreprocessingResultsKey];
if ([jsHelperResult valueForKey:@"iHateComputers"] == nil) {
return nil;
}
return [NSURL URLWithString:[jsHelperResult valueForKey:@"url"]];
} else if ([self.typeIdentifier isEqualToString:@"public.url"] == YES) {
return self.payload;
} if ([self.typeIdentifier isEqualToString:@"public.plain-text"] == YES) {
return [NSURL URLWithString:self.payload];
}
return nil;
}
@end

View File

@ -0,0 +1,23 @@
//
// HPSharedItemsContainer.h
// HotPocket
//
// Created by Tomek Wójcik on 27/09/2025.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class HPSharedItem;
@interface HPSharedItemsContainer : NSObject
@property (nullable) HPSharedItem *primaryItem;
@property NSMutableArray<HPSharedItem *> *candidateItems;
-(NSURL *)resolveURL;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,63 @@
//
// HPSharedItemsContainer.m
// HotPocket
//
// Created by Tomek Wójcik on 27/09/2025.
//
#import "HPSharedItemsContainer.h"
#import "HPSharedItem.h"
#import "NSURL+HotPocketExtensions.h"
@implementation HPSharedItemsContainer (HPSharedItemsContainerPrivate)
-(NSURL *)validatedURL:(NSURL *)url {
if (url.isUsableInHotPocket == NO) {
return nil;
}
return url;
}
@end
@implementation HPSharedItemsContainer
-(instancetype)init {
if (self = [super init]) {
self.primaryItem = nil;
self.candidateItems = [[NSMutableArray alloc] initWithCapacity:1];
}
return self;
}
-(NSURL *)resolveURL {
NSURL *result = nil;
if (self.primaryItem != nil) {
result = [self validatedURL:[self.primaryItem maybeURL]];
}
if ([self.candidateItems count] > 0) {
NSUInteger itemCandidateIndex = 0;
while (result == nil) {
HPSharedItem *itemCandidate = [self.candidateItems objectAtIndex:itemCandidateIndex];
result = [self validatedURL:itemCandidate.maybeURL];
if (result != nil) {
break;
}
itemCandidateIndex += 1;
if (itemCandidateIndex >= [self.candidateItems count]) {
break;
}
}
}
return result;
}
@end

View File

@ -7,6 +7,10 @@
#import <UIKit/UIKit.h>
@class HPAuthFlow;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonnull) HPAuthFlow *authFlow;
@end

View File

@ -7,14 +7,17 @@
#import "AppDelegate.h"
#import "HPAuthFlow.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.authFlow = [[HPAuthFlow alloc] init];
return YES;
}
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
-(UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}

View File

@ -0,0 +1,18 @@
//
// AuthorizationProgressViewController.h
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AuthorizationProgressViewController : UIViewController
@property IBOutlet UIActivityIndicatorView *progressIndicator;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,74 @@
//
// AuthorizationProgressViewController.m
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import "AuthorizationProgressViewController.h"
#import "AppDelegate.h"
#import "HPCredentialsHelper.h"
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
#pragma mark - Private interface
@end
@implementation AuthorizationProgressViewController
#pragma mark - View lifecycle
-(void)viewDidLoad {
[super viewDidLoad];
}
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.progressIndicator startAnimating];
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAuthFlowDidFinish:) name:@"AuthFlowDidFinish" object:appDelegate.authFlow];
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.progressIndicator stopAnimating];
}
#pragma mark - Notification handlers
-(void)onAuthFlowDidFinish:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
#ifdef DEBUG
NSLog(@"-[AuthorizationViewController onAuthFlowDidFinish:] notification=`%@`", notification);
#endif
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
if (credentials.usable == NO) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Oops!", @"Oops!")
message:NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.")
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Oh well", @"Oh well")
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[alert dismissViewControllerAnimated:YES completion:^{
[self.navigationController popViewControllerAnimated:YES];
}];
}]];
[self presentViewController:alert animated:YES completion:nil];
} else {
[self.navigationController popToRootViewControllerAnimated:YES];
}
});
}
@end

View File

@ -0,0 +1,22 @@
//
// AuthorizationViewController.h
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AuthorizationViewController : UIViewController
@property UIImageView *invalidURLWarningView;
@property IBOutlet UITextField *instanceURLField;
-(IBAction)doStartAuthorizationFlow:(id)sender;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,105 @@
//
// AuthorizationViewController.m
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import "AuthorizationViewController.h"
#import "AppDelegate.h"
#import "AuthorizationProgressViewController.h"
#import "HPAuthFlow.h"
#import "HPCredentialsHelper.h"
#import "MainViewController.h"
#import "NSURL+HotPocketExtensions.h"
@interface AuthorizationViewController (AuthorizationViewControllerPrivate)
#pragma mark - Private interface
@end
@implementation AuthorizationViewController
#pragma mark - View lifecycle
-(void)viewDidLoad {
[super viewDidLoad];
self.invalidURLWarningView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"exclamationmark.circle.fill"]];
self.invalidURLWarningView.contentMode = UIViewContentModeScaleAspectFit;
self.invalidURLWarningView.frame = CGRectMake(0, 0, 16, 16);
self.invalidURLWarningView.tintColor = [UIColor colorNamed:@"WarningColor"];
[self.view addSubview:self.invalidURLWarningView];
}
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.instanceURLField.rightView = self.invalidURLWarningView;
self.instanceURLField.rightViewMode = UITextFieldViewModeNever;
[self.instanceURLField addTarget:self action:@selector(onInstanceURLFieldChanged:) forControlEvents:UIControlEventEditingChanged];
}
#pragma mark - Actions
-(IBAction)doStartAuthorizationFlow:(id)sender {
#ifdef DEBUG
NSLog(@"-[AuthorizationViewController doStartAuthorizationFlow:] instanceURL=`%@`", self.instanceURLField.text);
#endif
if (!self.instanceURLField.text) {
// ???
return;
}
NSURL *instanceURL = [NSURL URLWithString:self.instanceURLField.text];
if (instanceURL.isUsableInHotPocket == NO) {
// ???
return;
}
UIApplication *application = [UIApplication sharedApplication];
AppDelegate *appDeleate = [application delegate];
appDeleate.authFlow.baseURL = instanceURL;
NSURL *authURL = [appDeleate.authFlow start];
if (authURL == nil) {
// ???
return;
}
if ([application canOpenURL:authURL] == YES) {
[application openURL:authURL options:@{} completionHandler:^(BOOL result) {
if (result == YES) {
AuthorizationProgressViewController *authorizationProgressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationProgressViewController"];
[self.navigationController pushViewController:authorizationProgressViewController animated:YES];
}
}];
}
}
#pragma mark - Event handlers
-(void)onInstanceURLFieldChanged:(UITextField *)sender {
sender.rightViewMode = UITextFieldViewModeNever;
if (!sender.text || [@"" isEqualToString:sender.text]) {
sender.rightViewMode = UITextFieldViewModeAlways;
return;
}
NSURL *url = [NSURL URLWithString:sender.text];
if (url == nil) {
sender.rightViewMode = UITextFieldViewModeAlways;
return;
}
if (url.isUsableInHotPocket == NO) {
sender.rightViewMode = UITextFieldViewModeAlways;
return;
}
}
@end

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24127" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
@ -17,11 +17,24 @@
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6HG-Um-bch">
<rect key="frame" x="131" y="363" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<imageReference key="image" image="LargeIcon"/>
</imageView>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="H2q-Qq-Nf1">
<rect key="frame" x="16" y="344" width="361" height="165"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6HG-Um-bch">
<rect key="frame" x="116" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<imageReference key="image" image="icon-mac-384.png"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HotPocket by BTHLabs" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="v6f-dw-6WO">
<rect key="frame" x="0.0" y="136" width="361" height="29"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="24"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" name="BackgroundColor"/>
@ -29,11 +42,11 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
<point key="canvasLocation" x="52.671755725190835" y="374.64788732394368"/>
</scene>
</scenes>
<resources>
<image name="LargeIcon" width="128" height="128"/>
<image name="icon-mac-384.png" width="384" height="384"/>
<namedColor name="BackgroundColor">
<color red="0.12941176470588237" green="0.14509803921568629" blue="0.16078431372549021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>

View File

@ -1,46 +1,245 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24127" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7Sa-RR-xgc">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<!--Main View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="MainViewController" id="BYZ-38-t0r" customClass="MainViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<wkWebView contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="RDB-ib-igF">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" name="BackgroundColor"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
</wkWebView>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Tdb-RK-EKV">
<rect key="frame" x="20" y="96" width="374" height="165"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="57V-kg-4Nx">
<rect key="frame" x="122" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<imageReference key="image" image="icon-mac-384.png"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HotPocket by BTHLabs" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cP6-uT-Hh4">
<rect key="frame" x="0.0" y="136" width="374" height="29"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="24"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HotPocket is configured and ready." textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ltc-Em-W9y" customClass="MultilineLabel">
<rect key="frame" x="20" y="277" width="374" height="64"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Instance URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GYw-2b-qGS">
<rect key="frame" x="20" y="349" width="374" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="OPO-AY-zgd">
<rect key="frame" x="20" y="378" width="374" height="35"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<buttonConfiguration key="configuration" style="gray" title="DO NOT LOCALIZE">
<color key="baseForegroundColor" name="SecondaryColor"/>
</buttonConfiguration>
<connections>
<action selector="doOpenInstanceURL:" destination="BYZ-38-t0r" eventType="primaryActionTriggered" id="5SW-3o-wiJ"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="wQZ-n6-b0o">
<rect key="frame" x="161" y="431" width="92" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonConfiguration key="configuration" style="gray" title="Log out">
<color key="baseForegroundColor" name="DangerColor"/>
</buttonConfiguration>
<connections>
<action selector="doLogOut:" destination="BYZ-38-t0r" eventType="primaryActionTriggered" id="iq7-wK-GMu"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" name="BackgroundColor"/>
<color key="tintColor" name="AccentColor"/>
</view>
<navigationItem key="navigationItem" id="w8s-f0-7E0"/>
<connections>
<outlet property="webView" destination="RDB-ib-igF" id="avx-RC-qRB"/>
<outlet property="instanceURLButton" destination="OPO-AY-zgd" id="1Wr-H9-eZ6"/>
<outlet property="logoutButton" destination="wQZ-n6-b0o" id="vco-vP-zvy"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
<point key="canvasLocation" x="962.31884057971024" y="375"/>
</scene>
<!--Authorization View Controller-->
<scene sceneID="zfn-5m-i4Y">
<objects>
<viewController storyboardIdentifier="AuthorizationViewController" id="1Il-xJ-X5Y" customClass="AuthorizationViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="gKn-cL-a2b">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Kkk-cr-Leu">
<rect key="frame" x="20" y="96" width="374" height="165"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YXT-lU-mHV">
<rect key="frame" x="122" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<imageReference key="image" image="icon-mac-384.png"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HotPocket by BTHLabs" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hhk-23-s8H">
<rect key="frame" x="0.0" y="136" width="374" height="29"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="24"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Instance URL" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="alG-Ve-nxN">
<rect key="frame" x="20" y="277" width="374" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="248" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="v5s-Uh-qWU" customClass="InstanceURLField">
<rect key="frame" x="20" y="306" width="374" height="34"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="URL" returnKeyType="go" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no" textContentType="url"/>
<connections>
<action selector="doStartAuthorizationFlow:" destination="1Il-xJ-X5Y" eventType="primaryActionTriggered" id="Rd9-1f-N6Z"/>
</connections>
</textField>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Enter the URL to your HotPocket instance, e.g. https://my.hotpocket.app" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tn1-fl-daL" customClass="MultilineLabel">
<rect key="frame" x="20" y="348" width="374" height="64"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eKt-N1-DEJ">
<rect key="frame" x="20" y="428" width="374" height="35"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="filled" title="Continue"/>
<connections>
<action selector="doStartAuthorizationFlow:" destination="1Il-xJ-X5Y" eventType="primaryActionTriggered" id="U0V-Pp-M2x"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="dL2-4T-yXY"/>
<color key="backgroundColor" name="BackgroundColor"/>
<color key="tintColor" name="AccentColor"/>
</view>
<connections>
<outlet property="instanceURLField" destination="v5s-Uh-qWU" id="hRQ-r8-3Dz"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="m6b-Bm-Ty7" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1726.0869565217392" y="375"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="zFJ-kU-27j">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="7Sa-RR-xgc" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="PrZ-Cz-0b5">
<rect key="frame" x="0.0" y="96" width="414" height="54"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="7mY-Zh-QsC"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="GIS-z2-loC" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="52.173913043478265" y="375"/>
</scene>
<!--Authorization Progress View Controller-->
<scene sceneID="689-0y-Gyr">
<objects>
<viewController storyboardIdentifier="AuthorizationProgressViewController" id="aiy-3v-nI7" customClass="AuthorizationProgressViewController" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="ljp-b5-lta">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wKr-WU-Iec">
<rect key="frame" x="20" y="96" width="374" height="165"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="v6u-sE-tzJ">
<rect key="frame" x="122" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<imageReference key="image" image="icon-mac-384.png"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HotPocket by BTHLabs" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="50v-cp-DGd">
<rect key="frame" x="0.0" y="136" width="374" height="29"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="24"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</view>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="DNy-gf-n60">
<rect key="frame" x="189" y="306" width="37" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Awaiting authentication response..." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qiJ-yx-nMd">
<rect key="frame" x="20" y="359" width="374" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="zyd-wv-1rn"/>
<color key="backgroundColor" name="BackgroundColor"/>
</view>
<connections>
<outlet property="progressIndicator" destination="DNy-gf-n60" id="hJF-jc-ZJ0"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="N3D-cM-5Ro" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2532" y="375"/>
</scene>
</scenes>
<color key="tintColor" name="AccentColor"/>
<resources>
<image name="icon-mac-384.png" width="384" height="384"/>
<namedColor name="AccentColor">
<color red="0.10980392156862745" green="0.72941176470588232" blue="0.92941176470588238" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="BackgroundColor">
<color red="0.12941176470588237" green="0.14509803921568629" blue="0.16078431372549021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="DangerColor">
<color red="0.93300002813339233" green="0.3919999897480011" blue="0.46299999952316284" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SecondaryColor">
<color red="0.0" green="0.50980392156862742" blue="0.8666666666666667" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -0,0 +1,15 @@
<?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.pl.bthlabs.HotPocket</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocketShared</string>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocket</string>
</array>
</dict>
</plist>

View File

@ -2,6 +2,32 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLIconFile</key>
<string>icon-mac-384</string>
<key>CFBundleURLName</key>
<string>HotPocketDesktopMac</string>
<key>CFBundleURLSchemes</key>
<array>
<string>hotpocket-mobile</string>
</array>
</dict>
</array>
<key>HPAuthFlowPostAuthenticateURLParts</key>
<dict>
<key>host</key>
<string>post-authenticate</string>
<key>scheme</key>
<string>hotpocket-mobile</string>
</dict>
<key>HPAuthFlowSource</key>
<string>HotPocketMobile</string>
<key>HPRPCClientOrigin</key>
<string>hotpocket-mobile://HPRPCClient</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>

View File

@ -0,0 +1,16 @@
//
// InstanceURLField.h
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 30/09/2025.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface InstanceURLField : UITextField
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,21 @@
//
// InstanceURLField.m
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 30/09/2025.
//
#import "InstanceURLField.h"
@implementation InstanceURLField
-(CGRect)rightViewRectForBounds:(CGRect)bounds {
if (self.rightViewMode != UITextFieldViewModeNever) {
CGFloat offsetTop = (bounds.size.height - 16.0) / 2.0;
return CGRectMake(bounds.size.width - 16.0 - offsetTop, offsetTop, 16.0, 16.0);
}
return CGRectNull;
}
@end

View File

@ -0,0 +1,22 @@
//
// MainViewController.h
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface MainViewController : UIViewController
@property IBOutlet UIButton *instanceURLButton;
@property IBOutlet UIButton *logoutButton;
-(IBAction)doOpenInstanceURL:(id)sender;
-(IBAction)doLogOut:(id)sender;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,77 @@
//
// MainViewController.m
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import "MainViewController.h"
#import "HPCredentialsHelper.h"
#import "AuthorizationViewController.h"
@interface MainViewController (MainViewControllerPrivate)
#pragma mark - Private interface
@end
@implementation MainViewController
#pragma mark - View lifecycle
-(void)viewDidLoad {
[super viewDidLoad];
[self.instanceURLButton setTitle:@"" forState:UIControlStateNormal];
self.instanceURLButton.enabled = NO;
self.logoutButton.enabled = NO;
}
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setNavigationBarHidden:YES animated:NO];
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
if (credentials.usable == NO) {
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationViewController"];
[self.navigationController pushViewController:authorizationViewController animated:NO];
} else {
[self.instanceURLButton setTitle:credentials.baseURL forState:UIControlStateNormal];
self.instanceURLButton.enabled = YES;
self.logoutButton.enabled = YES;
}
NSString *instanceURLText = @"";
if (credentials.baseURL != nil) {
instanceURLText = credentials.baseURL;
}
self.instanceURLButton.titleLabel.text = instanceURLText;
}
#pragma mark - Actions
-(IBAction)doOpenInstanceURL:(id)sender {
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
if (credentials.usable == YES) {
NSURL *instanceURL = [NSURL URLWithString:credentials.baseURL];
UIApplication *application = [UIApplication sharedApplication];
if ([application canOpenURL:instanceURL] == YES) {
[application openURL:instanceURL options:@{} completionHandler:nil];
}
}
}
-(IBAction)doLogOut:(id)sender {
[[HPCredentialsHelper sharedHelper] clearCredentials];
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationViewController"];
[self.navigationController pushViewController:authorizationViewController animated:NO];
}
@end

View File

@ -0,0 +1,16 @@
//
// MultilineLabel.h
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface MultilineLabel : UILabel
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,24 @@
//
// MultilineLabel.m
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import "MultilineLabel.h"
@implementation MultilineLabel
-(void)drawTextInRect:(CGRect)rect {
if (!self.text) {
[super drawTextInRect:rect];
return;
}
CGSize textSize = [self sizeThatFits:CGSizeMake(rect.size.width, CGFLOAT_MAX)];
CGRect textRect = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, MIN(rect.size.height, textSize.height));
[super drawTextInRect:textRect];
}
@end

View File

@ -7,6 +7,24 @@
#import "SceneDelegate.h"
#import "AppDelegate.h"
#import "HPAuthFlow.h"
@implementation SceneDelegate
-(void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
for (UIOpenURLContext *context in URLContexts) {
NSURL *url = context.URL;
HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url];
if (receivedAuthParams == nil) {
return;
}
[appDelegate.authFlow handleAuthParams:receivedAuthParams];
}
}
@end

View File

@ -0,0 +1,223 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24127" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="oWQ-SX-fOF">
<rect key="frame" x="20" y="118" width="353" height="165"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="OAy-89-4VU">
<rect key="frame" x="111" y="0.0" width="127" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<imageReference key="image" image="icon-mac-384.png"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HotPocket by BTHLabs" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ern-ld-Ivg">
<rect key="frame" x="-2" y="136" width="355" height="29"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="24"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</view>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zdu-vr-R6t" userLabel="Saving View">
<rect key="frame" x="20" y="299" width="353" height="142"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="1HO-hL-WcQ">
<rect key="frame" x="158" y="6" width="37" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CqC-1V-R49">
<rect key="frame" x="138" y="107" width="77" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="gray" title="Cancel">
<color key="baseForegroundColor" name="DangerColor"/>
</buttonConfiguration>
<connections>
<action selector="doCancel:" destination="j1y-V4-xli" eventType="primaryActionTriggered" id="EvP-s3-QOt"/>
</connections>
</button>
</subviews>
</view>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9Dr-s7-Kqv" userLabel="Done View">
<rect key="frame" x="20" y="299" width="353" height="142"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="checkmark.circle.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="0oo-rV-Vfd">
<rect key="frame" x="152" y="0.0" width="48" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" name="SuccessColor"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Your link has been saved!" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QoY-OC-nmX" customClass="MultilineLabel">
<rect key="frame" x="0.0" y="57" width="353" height="42"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="AHS-eb-Mk8">
<rect key="frame" x="143" y="107" width="67" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" name="AccentColor"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="Close"/>
<buttonConfiguration key="configuration" style="tinted" title="Close"/>
<connections>
<action selector="doClose:" destination="j1y-V4-xli" eventType="primaryActionTriggered" id="6GF-4h-9aj"/>
</connections>
</button>
</subviews>
</view>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DLn-U6-UcA" userLabel="Error View">
<rect key="frame" x="20" y="299" width="353" height="142"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="multiply.circle.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="HUo-6S-xfQ">
<rect key="frame" x="152" y="0.0" width="48" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" name="DangerColor"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HotPocket couldn't complete this operation." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="FFg-7U-S0i" customClass="MultilineLabel">
<rect key="frame" x="0.0" y="57" width="353" height="42"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="gFP-1G-Hef">
<rect key="frame" x="143" y="107" width="67" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" name="AccentColor"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="Close"/>
<buttonConfiguration key="configuration" style="tinted" title="Close"/>
<connections>
<action selector="doClose:" destination="j1y-V4-xli" eventType="primaryActionTriggered" id="Z85-xF-RxD"/>
</connections>
</button>
</subviews>
</view>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eqK-cv-2mf" userLabel="Needs Setup View">
<rect key="frame" x="20" y="299" width="353" height="142"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="exclamationmark.circle.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="fpS-az-ps2">
<rect key="frame" x="152" y="0.0" width="48" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" name="WarningColor"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Open the HotPocket App to set it up." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xjE-hq-bDA" customClass="MultilineLabel">
<rect key="frame" x="0.0" y="57" width="353" height="42"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="UJs-K5-SJe">
<rect key="frame" x="143" y="107" width="67" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" name="AccentColor"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="Close"/>
<buttonConfiguration key="configuration" style="tinted" title="Close"/>
<connections>
<action selector="doClose:" destination="j1y-V4-xli" eventType="primaryActionTriggered" id="40D-av-JQe"/>
</connections>
</button>
</subviews>
</view>
<view contentMode="scaleToFill" id="ckY-6z-0fe" userLabel="Unprocessable Entity VIew">
<rect key="frame" x="20" y="299" width="353" height="142"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="exclamationmark.circle.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="tMs-QS-o9E">
<rect key="frame" x="152" y="0.0" width="48" height="48"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" name="WarningColor"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="This item couldn't be shared :(." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tz8-Hp-Vha" customClass="MultilineLabel">
<rect key="frame" x="20" y="57" width="313" height="42"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0WL-Hk-6Bk">
<rect key="frame" x="143" y="107" width="67" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" name="AccentColor"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="Close"/>
<buttonConfiguration key="configuration" style="tinted" title="Close"/>
<connections>
<action selector="doClose:" destination="j1y-V4-xli" eventType="primaryActionTriggered" id="9pE-y5-xBp"/>
</connections>
</button>
</subviews>
<viewLayoutGuide key="safeArea" id="GQa-Md-2MI"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xfn-uj-KNk" userLabel="uname Label">
<rect key="frame" x="20" y="811" width="353" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
<color key="backgroundColor" name="BackgroundColor"/>
</view>
<connections>
<outlet property="doneView" destination="9Dr-s7-Kqv" id="Ote-sW-Adm"/>
<outlet property="errorView" destination="DLn-U6-UcA" id="cl1-I0-nY1"/>
<outlet property="needsSetupView" destination="eqK-cv-2mf" id="Vb3-Y3-8Y8"/>
<outlet property="progressIndicator" destination="1HO-hL-WcQ" id="1Qd-Gt-b3G"/>
<outlet property="savingView" destination="zdu-vr-R6t" id="svY-bA-Z9d"/>
<outlet property="unameLabel" destination="xfn-uj-KNk" id="TTp-PV-ttr"/>
<outlet property="unprocessableEntityView" destination="ckY-6z-0fe" id="owN-qJ-oBj"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="138.1679389312977" y="130.98591549295776"/>
</scene>
</scenes>
<resources>
<image name="checkmark.circle.fill" catalog="system" width="128" height="123"/>
<image name="exclamationmark.circle.fill" catalog="system" width="128" height="123"/>
<image name="icon-mac-384.png" width="384" height="384"/>
<image name="multiply.circle.fill" catalog="system" width="128" height="123"/>
<namedColor name="AccentColor">
<color red="0.10980392156862745" green="0.72941176470588232" blue="0.92941176470588238" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="BackgroundColor">
<color red="0.12941176470588237" green="0.14509803921568629" blue="0.16078431372549021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="DangerColor">
<color red="0.93300002813339233" green="0.3919999897480011" blue="0.46299999952316284" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SuccessColor">
<color red="0.054901960784313725" green="0.65490196078431373" blue="0.40392156862745099" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="WarningColor">
<color red="0.94509803921568625" green="0.58823529411764708" blue="0.1803921568627451" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -0,0 +1,27 @@
<?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>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>ShareExtensionHelper</string>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
</dict>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,20 @@
//
// ShareExtensionHelper.js
// HotPocket
//
// Created by Tomek Wójcik on 26/09/2025.
//
var ShareExtensionHelper = function() {
// OMG I CAN'T BELIEVE I HAVE TO EMBED JS IN THE SHARE EXTENSION :D
};
ShareExtensionHelper.prototype = {
run: function(arguments) {
arguments.completionFunction({
'iHateComputers': true,
'url': document.location.href,
});
},
};
var ExtensionPreprocessingJS = new ShareExtensionHelper();

View File

@ -0,0 +1,27 @@
//
// ShareViewController.h
// iOS (Share Extension)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import <UIKit/UIKit.h>
@class HPAPI;
@interface ShareViewController : UIViewController
@property HPAPI *api;
@property IBOutlet UIActivityIndicatorView *progressIndicator;
@property IBOutlet UIView *savingView;
@property IBOutlet UIView *needsSetupView;
@property IBOutlet UIView *doneView;
@property IBOutlet UIView *errorView;
@property IBOutlet UIView *unprocessableEntityView;
@property IBOutlet UILabel *unameLabel;
-(IBAction)doCancel:(id)sender;
-(IBAction)doClose:(id)sender;
@end

View File

@ -0,0 +1,126 @@
//
// ShareViewController.m
// iOS (Share Extension)
//
// Created by Tomek Wójcik on 25/09/2025.
//
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import "ShareViewController.h"
#import "HPAPI.h"
#import "HPCredentialsHelper.h"
#import "HPShareExtensionHelper.h"
@implementation ShareViewController (ShareViewControllerPrivate)
#pragma mark - Private interface
-(void)saveURL:(NSURL *)url {
#ifdef DEBUG
NSLog(@"-[ShareViewController save:] url=`%@`", url);
#endif
BOOL callResult = [self.api save:url completionHandler:^(NSString * _Nullable callId, HPRPCCallResult * _Nullable result) {
dispatch_async(dispatch_get_main_queue(), ^{
self.savingView.hidden = YES;
if (result.error != nil) {
#ifdef DEBUG
NSLog(@"-[ShareViewController resolveLinkAndSave] saveError=`%@`", result.error);
#endif
self.errorView.hidden = NO;
} else {
self.doneView.hidden = NO;
}
});
}];
if (callResult == NO) {
self.savingView.hidden = YES;
self.errorView.hidden = NO;
}
}
-(void)resolveLinkAndSave {
HPShareExtensionHelper *helper = [[HPShareExtensionHelper alloc] initWithContext:self.extensionContext];
[helper processItems:^(NSURL *url) {
if (url == nil) {
self.savingView.hidden = YES;
self.unprocessableEntityView.hidden = NO;
} else {
[self saveURL:url];
}
}];
}
@end
@implementation ShareViewController
#pragma mark - View lifecycle
-(void)viewDidLoad {
[super viewDidLoad];
self.savingView.hidden = NO;
self.needsSetupView.hidden = YES;
self.doneView.hidden = YES;
self.errorView.hidden = YES;
self.unprocessableEntityView.hidden = YES;
NSBundle *mainBundle = [NSBundle mainBundle];
self.unameLabel.text = [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]];
self.api = [[HPAPI alloc] init];
if (self.api.rpcClient.hasCredentials == YES) {
self.savingView.hidden = NO;
self.needsSetupView.hidden = YES;
} else {
self.savingView.hidden = YES;
self.needsSetupView.hidden = NO;
}
}
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.progressIndicator startAnimating];
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.progressIndicator stopAnimating];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.api checkAuth:^(BOOL authValid, NSError *error, NSString *callId) {
dispatch_async(dispatch_get_main_queue(), ^{
if (authValid == NO) {
#ifdef DEBUG
NSLog(@"-[ShareViewController viewDidAppear:] checkAuthError=`%@`", error);
#endif
self.savingView.hidden = YES;
self.needsSetupView.hidden = NO;
} else {
[self resolveLinkAndSave];
}
});
}];
}
#pragma mark - Actions
-(IBAction)doCancel:(id)sender {
NSError *cancelError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil];
[self.extensionContext cancelRequestWithError:cancelError];
}
-(IBAction)doClose:(id)sender {
NSExtensionItem *outputItem = [[NSExtensionItem alloc] init];
NSArray *outputItems = @[outputItem];
[self.extensionContext completeRequestReturningItems:outputItems completionHandler:nil];
}
@end

View File

@ -0,0 +1,15 @@
<?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.pl.bthlabs.HotPocket</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocketShared</string>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocket.ShareExtension</string>
</array>
</dict>
</plist>

View File

@ -3,3 +3,4 @@ run:
pty: true
files_to_version:
- "HotPocket.xcodeproj/project.pbxproj"
- "pyproject.toml"

View File

@ -7,6 +7,10 @@
#import <Cocoa/Cocoa.h>
@class HPAuthFlow;
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (strong, nonnull) HPAuthFlow *authFlow;
@end

View File

@ -7,14 +7,32 @@
#import "AppDelegate.h"
#import "HPAuthFlow.h"
#import "HPCredentialsHelper.h"
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
// Override point for customization after application launch.
-(void)applicationDidFinishLaunching:(NSNotification *)notification {
self.authFlow = [[HPAuthFlow alloc] init];
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
-(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
return YES;
}
-(void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
HPAuthParams *receivedAuthParams = nil;
for (NSURL *url in urls) {
receivedAuthParams = [self.authFlow handlePostAuthenticateURL:url];
if (receivedAuthParams != nil) {
break;
}
}
if (receivedAuthParams != nil) {
[self.authFlow handleAuthParams:receivedAuthParams];
}
}
@end

View File

@ -0,0 +1,18 @@
//
// AuthorizationProgressViewController.h
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 20/09/2025.
//
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface AuthorizationProgressViewController : NSViewController
@property IBOutlet NSProgressIndicator *progressIndicator;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,68 @@
//
// AuthorizationProgressViewController.m
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 20/09/2025.
//
#import "AuthorizationProgressViewController.h"
#import "AppDelegate.h"
#import "AuthorizationViewController.h"
#import "HPCredentialsHelper.h"
#import "MainViewController.h"
#import "ReplaceAnimator.h"
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
#pragma mark - Private interface
@end
@implementation AuthorizationProgressViewController
#pragma mark - View lifecycle
-(void)viewDidLoad {
[super viewDidLoad];
}
-(void)viewWillAppear {
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAuthFlowDidFinish:) name:@"AuthFlowDidFinish" object:appDelegate.authFlow];
[self.progressIndicator startAnimation:self];
}
-(void)viewDidDisappear {
[self.progressIndicator stopAnimation:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Notification handlers
-(void)onAuthFlowDidFinish:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
[[NSApplication sharedApplication] requestUserAttention:NSInformationalRequest];
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
if (credentials.usable == NO) {
NSAlert *alert = [[NSAlert alloc] init];
alert.alertStyle = NSAlertStyleCritical;
alert.messageText = NSLocalizedString(@"Oops!", @"Oops!");
alert.informativeText = NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.");
[alert runModal];
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
} else {
MainViewController *mainViewController = [self.storyboard instantiateControllerWithIdentifier:@"MainViewController"];
[self presentViewController:mainViewController animator:[[ReplaceAnimator alloc] init]];
}
});
}
@end

View File

@ -0,0 +1,21 @@
//
// AuthorizationViewController.h
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 20/09/2025.
//
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface AuthorizationViewController : NSViewController
@property (nullable) NSString *baseURL;
@property (nullable) NSString *authorizationSessionToken;
-(IBAction)doStartAuthorizationFlow:(id)sender;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,49 @@
//
// AuthorizationViewController.m
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 20/09/2025.
//
#import "AuthorizationViewController.h"
#import "AppDelegate.h"
#import "HPAuthFlow.h"
#import "AuthorizationProgressViewController.h"
#import "ReplaceAnimator.h"
@interface AuthorizationViewController (AuthorizationViewControllerPrivate)
#pragma mark - Private interface
@end
@implementation AuthorizationViewController
#pragma mark - View lifecycle
-(void)viewDidLoad {
[super viewDidLoad];
self.baseURL = nil;
self.authorizationSessionToken = nil;
}
#pragma mark - Actions
-(IBAction)doStartAuthorizationFlow:(id)sender {
AppDelegate *appDeleate = [[NSApplication sharedApplication] delegate];
appDeleate.authFlow.baseURL = [NSURL URLWithString:self.baseURL];
NSURL *authURL = [appDeleate.authFlow start];
if (authURL == nil) {
NSBeep();
return;
}
AuthorizationProgressViewController *authProgressViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationProgressViewController"];
[self presentViewController:authProgressViewController animator:[[ReplaceAnimator alloc] init]];
[[NSWorkspace sharedWorkspace] openURL:authURL];
}
@end

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="24127" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/>
<plugIn identifier="com.apple.WebKit2IBPlugin" version="23727"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24127"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -77,7 +77,7 @@
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController showSeguePresentationStyle="single" id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="HotPocket" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA">
<window key="window" title="HotPocket" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" animationBehavior="default" toolbarStyle="expanded" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
<rect key="contentRect" x="196" y="240" width="425" height="325"/>
@ -87,38 +87,216 @@
</connections>
</window>
<connections>
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
<segue destination="r5D-xE-cNT" kind="relationship" relationship="window.shadowedContentViewController" id="j8b-cd-GSP"/>
</connections>
</windowController>
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="250"/>
</scene>
<!--View Controller-->
<!--Authorization View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" sceneMemberID="viewController">
<view key="view" appearanceType="darkAqua" id="m2S-Jp-Qdl">
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<viewController storyboardIdentifier="AuthorizationViewController" id="XfG-lQ-9wD" customClass="AuthorizationViewController" sceneMemberID="viewController">
<view key="view" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<wkWebView wantsLayer="YES" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="eOr-cG-IQY">
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
<wkPreferences key="preferences"/>
</wkWebViewConfiguration>
</wkWebView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7sM-F3-Zzf">
<rect key="frame" x="18" y="153" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="HotPocket Instance URL" id="XwM-DV-kei">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ygC-xe-m6y">
<rect key="frame" x="20" y="124" width="385" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" id="rHK-hP-yWO">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="XfG-lQ-9wD" name="value" keyPath="self.baseURL" id="OhE-52-yPd">
<dictionary key="options">
<bool key="NSContinuouslyUpdatesValue" value="YES"/>
</dictionary>
</binding>
</connections>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DIc-8O-uoQ">
<rect key="frame" x="18" y="68" width="389" height="48"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" title="Enter the URL to your HotPocket instance, e.g. https://my.hotpocket.app" id="Y0q-a1-oBP">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="PAH-U3-ltl">
<rect key="frame" x="180" y="221" width="64" height="64"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="icon-mac-384" id="jnV-K2-7gf"/>
</imageView>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2Zr-9i-XDS">
<rect key="frame" x="13" y="33" width="89" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Continue" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="HBR-3P-qCC">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<color key="contentTintColor" name="AccentColor"/>
<connections>
<action selector="doStartAuthorizationFlow:" target="XfG-lQ-9wD" id="AOi-Wt-gmL"/>
</connections>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mQc-Ea-NNN">
<rect key="frame" x="18" y="185" width="389" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="NTZ-zl-yhk">
<font key="font" metaFont="system" size="24"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
</viewController>
</objects>
<point key="canvasLocation" x="606" y="1067"/>
</scene>
<!--Authorization Progress View Controller-->
<scene sceneID="HWJ-b5-BG6">
<objects>
<viewController storyboardIdentifier="AuthorizationProgressViewController" id="OX4-Oj-1cw" customClass="AuthorizationProgressViewController" sceneMemberID="viewController">
<view key="view" id="qln-dC-eog">
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yRj-hC-QYS">
<rect key="frame" x="18" y="185" width="389" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="F4l-2Z-D79">
<font key="font" metaFont="system" size="24"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ca6-br-wxp">
<rect key="frame" x="180" y="221" width="64" height="64"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="icon-mac-384" id="faZ-e6-z8R"/>
</imageView>
<progressIndicator fixedFrame="YES" maxValue="100" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="51a-ii-yug">
<rect key="frame" x="196" y="113" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g9a-gR-c7o">
<rect key="frame" x="18" y="81" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Awaiting authorization response..." id="3oi-LK-vKv">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
<connections>
<outlet property="webView" destination="eOr-cG-IQY" id="GFe-mU-dBY"/>
<outlet property="progressIndicator" destination="51a-ii-yug" id="hWy-Hb-3pE"/>
</connections>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<customObject id="Dav-PG-FiQ" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="655"/>
<point key="canvasLocation" x="605.5" y="654.5"/>
</scene>
<!--Main View Controller-->
<scene sceneID="UJA-e4-0rA">
<objects>
<viewController storyboardIdentifier="MainViewController" id="r5D-xE-cNT" customClass="MainViewController" sceneMemberID="viewController">
<view key="view" id="jVt-oL-KPZ">
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ybL-DV-U73">
<rect key="frame" x="180" y="221" width="64" height="64"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyDown" image="icon-mac-384" id="fae-mz-0sj"/>
</imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T7q-KB-3Ut">
<rect key="frame" x="18" y="185" width="389" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="r5O-Sk-IdK">
<font key="font" metaFont="system" size="24"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2h7-bN-dsa">
<rect key="frame" x="18" y="121" width="389" height="48"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" title="HotPocket is configured and ready." id="5fh-mh-WR1">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uci-UC-wxo">
<rect key="frame" x="18" y="97" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Instance URL" id="azk-ea-KeN">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LR4-eJ-jlA">
<rect key="frame" x="13" y="30" width="80" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Log out" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="LDr-35-Wph">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<color key="contentTintColor" name="DangerColor"/>
<connections>
<action selector="doLogOut:" target="r5D-xE-cNT" id="BMt-Zp-8v5"/>
<binding destination="r5D-xE-cNT" name="enabled" keyPath="self.logoutButtonEnabled" id="gTs-BO-USz"/>
</connections>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8H3-oU-acU" customClass="LinkLabel">
<rect key="frame" x="18" y="73" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" allowsEditingTextAttributes="YES" id="EoA-mM-phM">
<font key="font" metaFont="system"/>
<color key="textColor" name="SecondaryColor"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
<connections>
<outlet property="instanceURLLabel" destination="8H3-oU-acU" id="EuY-xr-zar"/>
</connections>
</viewController>
<customObject id="dlo-Mj-lGf" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="74.5" y="654.5"/>
</scene>
</scenes>
<resources>
<image name="icon-mac-384" width="384" height="384"/>
<namedColor name="AccentColor">
<color red="0.10980392156862745" green="0.72941176470588232" blue="0.92941176470588238" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="DangerColor">
<color red="0.93300002813339233" green="0.3919999897480011" blue="0.46299999952316284" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SecondaryColor">
<color red="0.0" green="0.50980392156862742" blue="0.8666666666666667" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -4,9 +4,18 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.pl.bthlabs.HotPocket</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocketShared</string>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocket</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,32 @@
<?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>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLIconFile</key>
<string>icon-mac-384</string>
<key>CFBundleURLName</key>
<string>HotPocketDesktopMac</string>
<key>CFBundleURLSchemes</key>
<array>
<string>hotpocket-desktop</string>
</array>
</dict>
</array>
<key>HPAuthFlowPostAuthenticateURLParts</key>
<dict>
<key>host</key>
<string>post-authenticate</string>
<key>scheme</key>
<string>hotpocket-desktop</string>
</dict>
<key>HPAuthFlowSource</key>
<string>HotPocketDesktop</string>
<key>HPRPCClientOrigin</key>
<string>hotpocket-desktop://HPRPCClient</string>
</dict>
</plist>

View File

@ -0,0 +1,16 @@
//
// LinkLabel.h
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 24/09/2025.
//
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface LinkLabel : NSTextField
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,24 @@
//
// LinkLabel.m
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 24/09/2025.
//
#import "LinkLabel.h"
@implementation LinkLabel
-(void)awakeFromNib {
[super awakeFromNib];
self.allowsEditingTextAttributes = YES;
self.textColor = [NSColor colorNamed:@"SecondaryColor"];
self.selectable = YES;
}
-(void)resetCursorRects {
[super resetCursorRects];
[self addCursorRect:self.bounds cursor:NSCursor.pointingHandCursor];
}
@end

View File

@ -0,0 +1,22 @@
//
// MainViewController.h
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 22/09/2025.
//
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface MainViewController : NSViewController
@property IBOutlet NSTextField *instanceURLLabel;
@property BOOL logoutButtonEnabled;
-(IBAction)doLogOut:(id)sender;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,65 @@
//
// MainViewController.m
// HotPocket (iOS)
//
// Created by Tomek Wójcik on 22/09/2025.
//
#import "MainViewController.h"
#import "HPCredentialsHelper.h"
#import "AuthorizationViewController.h"
#import "ReplaceAnimator.h"
@interface MainViewController (MainViewControllerPrivate)
#pragma mark - Private interface
@end
@implementation MainViewController
#pragma mark - View lifecycle
-(void)viewDidLoad {
[super viewDidLoad];
self.logoutButtonEnabled = NO;
}
-(void)viewDidAppear {
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
if (credentials.usable == NO) {
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
} else {
self.logoutButtonEnabled = YES;
}
NSString *instanceURLText = @"";
if (credentials.baseURL != nil) {
instanceURLText = credentials.baseURL;
}
NSMutableAttributedString *instanceURLValue = [[NSMutableAttributedString alloc] initWithString:instanceURLText];
if (credentials.baseURL != nil) {
[instanceURLValue addAttribute:NSLinkAttributeName
value:credentials.baseURL
range:NSMakeRange(0, instanceURLValue.length)];
}
[instanceURLValue addAttribute:NSUnderlineStyleAttributeName
value:[NSNumber numberWithInteger:NSUnderlineStyleSingle]
range:NSMakeRange(0, instanceURLValue.length)];
self.instanceURLLabel.attributedStringValue = instanceURLValue;
}
#pragma mark - Actions
-(IBAction)doLogOut:(id)sender {
[[HPCredentialsHelper sharedHelper] clearCredentials];
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
}
@end

View File

@ -0,0 +1,16 @@
//
// ReplaceAnimator.h
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 20/09/2025.
//
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface ReplaceAnimator : NSObject<NSViewControllerPresentationAnimator>
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,29 @@
//
// ReplaceAnimator.m
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 20/09/2025.
//
#import "ReplaceAnimator.h"
@implementation ReplaceAnimator
-(void)animatePresentationOfViewController:(NSViewController *)viewController fromViewController:(NSViewController *)fromViewController {
NSView *container = fromViewController.view.superview;
if (container == nil) {
return;
}
[fromViewController.view removeFromSuperview];
[container addSubview:viewController.view];
viewController.view.frame = NSMakeRect(0, 0, viewController.view.frame.size.width, viewController.view.frame.size.height);
viewController.view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
}
-(void)animateDismissalOfViewController:(NSViewController *)viewController fromViewController:(NSViewController *)fromViewController {
[viewController.view removeFromSuperview];
}
@end

View File

@ -0,0 +1,19 @@
//
// WindowContentView.h
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 30/09/2025.
//
#import <Cocoa/Cocoa.h>
NS_ASSUME_NONNULL_BEGIN
@interface WindowContentView : NSView
@property (strong) NSColor *darkBackgroundColor;
@property (strong) NSColor *lightBackgroundColor;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,42 @@
//
// WindowContentView.m
// HotPocket (macOS)
//
// Created by Tomek Wójcik on 30/09/2025.
//
#import "WindowContentView.h"
@implementation WindowContentView
-(void)awakeFromNib {
[super awakeFromNib];
self.darkBackgroundColor = [NSColor colorNamed:@"BackgroundColor"];
self.lightBackgroundColor = [NSColor windowBackgroundColor];
}
-(BOOL)isOpaque {
return YES;
}
-(void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSAppearance *appearance = self.effectiveAppearance;
NSAppearanceName bestMatch = [appearance bestMatchFromAppearancesWithNames:@[
NSAppearanceNameAqua,
NSAppearanceNameDarkAqua
]];
NSColor *backgroundColor = self.lightBackgroundColor;
if ([bestMatch isEqualToString:NSAppearanceNameDarkAqua] == YES) {
backgroundColor = self.darkBackgroundColor;
}
if (backgroundColor) {
[backgroundColor setFill];
NSRectFill(dirtyRect);
}
}
@end

View File

@ -2,9 +2,18 @@
<!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.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.pl.bthlabs.HotPocket</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocketShared</string>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocket.Extension</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,245 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24127" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24127"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors introduced in macOS 10.14" minToolsVersion="10.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ShareViewController">
<connections>
<outlet property="progressIndicator" destination="3hY-1S-Wo2" id="Dpk-yi-LVA"/>
<outlet property="view" destination="1" id="2"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="1">
<rect key="frame" x="0.0" y="0.0" width="388" height="258"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="339-Mg-wWu">
<rect key="frame" x="163" y="174" width="64" height="64"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyDown" image="icon-mac-384" id="NT0-XU-t9f"/>
</imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KZW-gY-pvX">
<rect key="frame" x="18" y="138" width="352" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="urI-Z1-yMm">
<font key="font" metaFont="system" size="24"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<customView id="ERc-i0-Lfz" userLabel="Error View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Hwr-tf-MxL">
<rect key="frame" x="140" y="-7" width="69" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="7Kd-FS-0AY">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancel:" target="-2" id="yRt-GR-jQ6"/>
</connections>
</button>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5X8-4n-wWm">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="HotPocket couldn't complete this operation." id="fmg-RT-3FA">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" id="95C-iI-GaL">
<rect key="frame" x="158" y="65" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="multiply.circle.fill" catalog="system" id="iXK-aQ-lIy"/>
<color key="contentTintColor" name="DangerColor"/>
</imageView>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.errorViewHidden" id="WUX-Pk-WLH"/>
</connections>
</customView>
<customView id="lQA-9N-11c" userLabel="Done View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" id="QBX-Dv-VAz">
<rect key="frame" x="158" y="65" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="checkmark.circle.fill" catalog="system" id="iFW-1s-X0l"/>
<color key="contentTintColor" name="SuccessColor"/>
</imageView>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DFX-X0-zX8">
<rect key="frame" x="140" y="-7" width="69" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="2jG-9M-YQb">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="close:" target="-2" id="3aP-Lu-EzX"/>
</connections>
</button>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Mfx-pW-oi2">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="Your link has been saved!" id="JhJ-K4-UFb">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.doneViewHidden" id="4mN-62-nDm"/>
</connections>
</customView>
<customView id="XMP-x8-OMJ" userLabel="Needs Setup View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" id="p8k-QM-ZwX">
<rect key="frame" x="158" y="65" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyUpOrDown" image="exclamationmark.circle.fill" catalog="system" id="3kO-Gq-csg"/>
<color key="contentTintColor" name="WarningColor"/>
</imageView>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YLC-Bx-qKZ">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="Open the HotPocket application to set it up." id="eYb-eq-cbo">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ufJ-HY-8Ir">
<rect key="frame" x="136" y="-7" width="76" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="aH5-RV-e1O">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancel:" target="-2" id="ZRZ-Es-NM6"/>
</connections>
</button>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.needsSetupViewHidden" id="zQw-vW-9Y4"/>
</connections>
</customView>
<customView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Szo-4N-z5M" userLabel="Saving View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="b64-XQ-Anx">
<rect key="frame" x="136" y="-7" width="76" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="7U4-so-kvt">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancel:" target="-2" id="Aia-vP-eiA"/>
</connections>
</button>
<progressIndicator fixedFrame="YES" maxValue="100" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="3hY-1S-Wo2">
<rect key="frame" x="158" y="52" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.savingViewHidden" id="HNo-aa-0OR"/>
</connections>
</customView>
<customView id="cFw-BG-nDd" userLabel="Unprocessable Entity View">
<rect key="frame" x="20" y="22" width="348" height="100"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" id="hQi-Zt-thw">
<rect key="frame" x="158" y="65" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyUpOrDown" image="exclamationmark.circle.fill" catalog="system" id="66K-cT-2Vw"/>
<color key="contentTintColor" name="WarningColor"/>
</imageView>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LS4-qN-h75">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="This item couldn't be shared :(." id="b0i-Lf-21f">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cDA-ec-njT">
<rect key="frame" x="136" y="-7" width="76" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="lr2-gc-RFv">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="cancel:" target="-2" id="yWF-hM-ejy"/>
</connections>
</button>
</subviews>
<connections>
<binding destination="-2" name="hidden" keyPath="self.unprocessableEntityViewHidden" id="lqC-lO-ll8"/>
</connections>
</customView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1yJ-sU-Spr" userLabel="uname Label">
<rect key="frame" x="6" y="4" width="376" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="clipping" alignment="center" id="nQ0-Es-oIB">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="controlAccentColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<connections>
<binding destination="-2" name="value" keyPath="self.uname" id="rKp-CV-Mpj"/>
</connections>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="132" y="-45"/>
</customView>
</objects>
<resources>
<image name="checkmark.circle.fill" catalog="system" width="15" height="15"/>
<image name="exclamationmark.circle.fill" catalog="system" width="15" height="15"/>
<image name="icon-mac-384" width="384" height="384"/>
<image name="multiply.circle.fill" catalog="system" width="15" height="15"/>
<namedColor name="DangerColor">
<color red="0.93300002813339233" green="0.3919999897480011" blue="0.46299999952316284" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SuccessColor">
<color red="0.054901960784313725" green="0.65490196078431373" blue="0.40392156862745099" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="WarningColor">
<color red="0.94509803921568625" green="0.58823529411764708" blue="0.1803921568627451" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -0,0 +1,29 @@
<?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>CFBundleIconFile</key>
<string>icon</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>ShareExtensionHelper</string>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>ShareViewController</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,24 @@
//
// ShareViewController.h
// macOS (Share Extension)
//
// Created by Tomek Wójcik on 22/09/2025.
//
#import <Cocoa/Cocoa.h>
@class HPAPI;
@interface ShareViewController : NSViewController
@property HPAPI *api;
@property BOOL savingViewHidden;
@property BOOL needsSetupViewHidden;
@property BOOL doneViewHidden;
@property BOOL errorViewHidden;
@property BOOL unprocessableEntityViewHidden;
@property NSString *uname;
@property IBOutlet NSProgressIndicator *progressIndicator;
@end

View File

@ -0,0 +1,128 @@
//
// ShareViewController.m
// macOS (Share Extension)
//
// Created by Tomek Wójcik on 22/09/2025.
//
#import "ShareViewController.h"
#import "HPAPI.h"
#import "HPShareExtensionHelper.h"
@implementation ShareViewController (ShareViewControllerPrivate)
#pragma mark - Private interface
-(void)saveURL:(NSURL *)url {
#ifdef DEBUG
NSLog(@"-[ShareViewController save:] url=`%@`", url);
#endif
BOOL callResult = [self.api save:url completionHandler:^(NSString * _Nullable callId, HPRPCCallResult * _Nullable result) {
dispatch_async(dispatch_get_main_queue(), ^{
self.savingViewHidden = YES;
if (result.error != nil) {
#ifdef DEBUG
NSLog(@"-[ShareViewController resolveLinkAndSave] saveError=`%@`", result.error);
#endif
self.errorViewHidden = NO;
} else {
self.doneViewHidden = NO;
}
});
}];
if (callResult == NO) {
self.savingViewHidden = YES;
self.errorViewHidden = NO;
}
}
-(void)resolveLinkAndSave {
HPShareExtensionHelper *helper = [[HPShareExtensionHelper alloc] initWithContext:self.extensionContext];
[helper processItems:^(NSURL *url) {
if (url == nil) {
self.savingViewHidden = YES;
self.unprocessableEntityViewHidden = NO;
} else {
[self saveURL:url];
}
}];
}
@end
@implementation ShareViewController
#pragma mark - View lifecycle
-(NSString *)nibName {
return @"ShareViewController";
}
-(void)viewDidLoad {
[super viewDidLoad];
self.savingViewHidden = NO;
self.needsSetupViewHidden = YES;
self.doneViewHidden = YES;
self.errorViewHidden = YES;
self.unprocessableEntityViewHidden = YES;
NSBundle *mainBundle = [NSBundle mainBundle];
self.uname = [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]];
self.api = [[HPAPI alloc] init];
if (self.api.rpcClient.hasCredentials == YES) {
self.savingViewHidden = NO;
self.needsSetupViewHidden = YES;
} else {
self.savingViewHidden = YES;
self.needsSetupViewHidden = NO;
}
}
-(void)viewWillAppear {
[super viewWillAppear];
[self.progressIndicator startAnimation:self];
}
-(void)viewDidAppear {
[super viewDidAppear];
[self.api checkAuth:^(BOOL authValid, NSError *error, NSString *callId) {
dispatch_async(dispatch_get_main_queue(), ^{
if (authValid == NO) {
#ifdef DEBUG
NSLog(@"-[ShareViewController viewDidAppear:] checkAuthError=`%@`", error);
#endif
self.savingViewHidden = YES;
self.needsSetupViewHidden = NO;
} else {
[self resolveLinkAndSave];
}
});
}];
}
-(void)viewDidDisappear {
[super viewDidDisappear];
[self.progressIndicator stopAnimation:self];
}
#pragma mark - Actions
-(IBAction)close:(id)sender {
NSExtensionItem *outputItem = [[NSExtensionItem alloc] init];
NSArray *outputItems = @[outputItem];
[self.extensionContext completeRequestReturningItems:outputItems completionHandler:nil];
}
-(IBAction)cancel:(id)sender {
NSError *cancelError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil];
[self.extensionContext cancelRequestWithError:cancelError];
}
@end

Binary file not shown.

View File

@ -0,0 +1,21 @@
<?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.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.pl.bthlabs.HotPocket</string>
</array>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocketShared</string>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocket.ShareExtension</string>
</array>
</dict>
</plist>

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "hotpocket-apple"
version = "25.9.12"
version = "25.9.17"
description = "HotPocket Apple Integrations"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0"

View File

@ -0,0 +1,29 @@
# Generated by Django 5.2.3 on 2025-09-22 07:20
import uuid6
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0005_accesstoken'),
]
operations = [
migrations.CreateModel(
name='AuthKey',
fields=[
('id', models.UUIDField(default=uuid6.uuid7, editable=False, primary_key=True, serialize=False)),
('account_uuid', models.UUIDField(db_index=True, default=None)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('deleted_at', models.DateTimeField(blank=True, db_index=True, default=None, editable=False, null=True)),
('key', models.CharField(db_index=True, default=None, editable=False, max_length=128, unique=True)),
],
options={
'verbose_name': 'Auth Key',
'verbose_name_plural': 'Auth Keys',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.3 on 2025-10-01 07:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0006_authkey'),
]
operations = [
migrations.AddField(
model_name='authkey',
name='consumed_at',
field=models.DateTimeField(blank=True, db_index=True, default=None, editable=False, null=True),
),
]

View File

@ -1,2 +1,3 @@
from .access_token import AccessToken # noqa: F401
from .account import Account # noqa: F401
from .auth_key import AuthKey # noqa: F401

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.db import models
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.core.models import Model
class ActiveAuthKeysManager(models.Manager):
def get_queryset(self) -> models.QuerySet[AuthKey]:
return super().get_queryset().filter(
deleted_at__isnull=True,
)
class AuthKey(Model):
key = models.CharField(
blank=False,
default=None,
null=False,
max_length=128,
db_index=True,
unique=True,
editable=False,
)
consumed_at = models.DateTimeField(
blank=True,
null=True,
default=None,
db_index=True,
editable=False,
)
objects = models.Manager()
active_objects = ActiveAuthKeysManager()
class Meta:
verbose_name = _('Auth Key')
verbose_name_plural = _('Auth Keys')
def __str__(self) -> str:
return f'<AuthKey pk={self.pk} key={self.key}>'

View File

@ -1 +1,3 @@
from .access_tokens import AccessTokensService # noqa: F401
from .accounts import AccountsService # noqa: F401
from .auth_keys import AuthKeysService # noqa: F401

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
import uuid
from hotpocket_backend.apps.accounts.models import Account
LOGGER = logging.getLogger(__name__)
class AccountsService:
class AccountsServiceError(Exception):
pass
class AccountNotFound(AccountsServiceError):
pass
def get(self, *, pk: uuid.UUID) -> Account:
try:
query_set = Account.objects.filter(is_active=True)
return query_set.get(pk=pk)
except Account.DoesNotExist as exception:
raise self.AccountNotFound(
f'Account not found: pk=`{pk}`',
) from exception

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import datetime
import logging
import uuid
from django.utils.timezone import now
import uuid6
from hotpocket_backend.apps.accounts.models import AuthKey
from hotpocket_backend.apps.core.conf import settings
LOGGER = logging.getLogger(__name__)
class AuthKeysService:
class AuthKeysServiceError(Exception):
pass
class AuthKeyNotFound(AuthKeysServiceError):
pass
class AuthKeyExpired(AuthKeysServiceError):
pass
class AuthKeyAccessDenied(AuthKeysServiceError):
pass
def create(self, *, account_uuid: uuid.UUID) -> AuthKey:
key = str(uuid6.uuid7())
return AuthKey.objects.create(
account_uuid=account_uuid,
key=key,
)
def get(self, *, pk: uuid.UUID) -> AuthKey:
try:
query_set = AuthKey.active_objects
return query_set.get(pk=pk)
except AuthKey.DoesNotExist as exception:
raise self.AuthKeyNotFound(
f'Auth Key not found: pk=`{pk}`',
) from exception
def get_by_key(self, *, key: str, ttl: int | None = None) -> AuthKey:
try:
query_set = AuthKey.active_objects
result = query_set.get(key=key)
if ttl is None:
ttl = settings.AUTH_KEY_TTL
if ttl > 0:
if result.created_at < now() - datetime.timedelta(seconds=ttl):
raise self.AuthKeyExpired(
f'Auth Key expired: pk=`{key}`',
)
if result.consumed_at is not None:
raise self.AuthKeyExpired(
f'Auth Key already consumed: pk=`{key}`',
)
return result
except AuthKey.DoesNotExist as exception:
raise self.AuthKeyNotFound(
f'Auth Key not found: key=`{key}`',
) from exception

View File

@ -30,3 +30,5 @@ class PSettings(typing.Protocol):
SAVES_ASSOCIATION_ADAPTER: str
UPLOADS_PATH: pathlib.Path
AUTH_KEY_TTL: int

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.3 on 2025-10-01 05:35
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('saves', '0007_association_target_description_and_more'),
]
operations = [
migrations.AlterField(
model_name='save',
name='url',
field=models.CharField(default=None, validators=[django.core.validators.URLValidator(schemes=['http', 'https'])]),
),
]

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.core import validators
from django.db import models
from django.utils.translation import gettext_lazy as _
@ -20,6 +21,9 @@ class Save(Model):
)
url = models.CharField(
blank=False, null=False, default=None,
validators=[
validators.URLValidator(schemes=['http', 'https']),
],
)
content = models.BinaryField(
blank=True, null=True, default=None, editable=False,

View File

@ -23,3 +23,11 @@ class UIAccessTokenOriginApp(enum.Enum):
SAFARI_WEB_EXTENSION = _('Safari Web Extension')
CHROME_EXTENSION = _('Chrome Extension')
FIREFOX_EXTENSION = _('Firefox Extension')
HOTPOCKET_DESKTOP = _('HotPocket Desktop')
HOTPOCKET_MOBILE = _('HotPocket Mobile')
class AuthSource(enum.Enum):
BROWSER_EXTENSION = 'HotPocketExtension'
DESKTOP = 'HotPocketDesktop'
MOBILE = 'HotPocketMobile'

View File

@ -7,23 +7,37 @@ from bthlabs_jsonrpc_core import register_method
from django import db
from django.http import HttpRequest
from hotpocket_soa.services import AccessTokensService
from hotpocket_soa.services import (
AccessTokensService,
AccountsService,
AuthKeysService,
)
LOGGER = logging.getLogger(__name__)
@register_method('accounts.access_tokens.create')
@register_method('accounts.access_tokens.create', namespace='accounts')
def create(request: HttpRequest,
auth_key: str,
meta: dict,
) -> str:
with db.transaction.atomic():
try:
assert 'extension_auth_key' in request.session, 'Auth key missing'
assert request.session['extension_auth_key'] == auth_key, (
'Auth key mismatch'
auth_key_object = AuthKeysService().get_by_key(
account_uuid=None,
key=auth_key,
)
except AssertionError as exception:
except AuthKeysService.AuthKeyNotFound as exception:
LOGGER.error(
'Unable to issue access token: %s',
exception,
exc_info=exception,
)
raise
try:
account = AccountsService().get(pk=auth_key_object.account_uuid)
except AccountsService.AccountNotFound as exception:
LOGGER.error(
'Unable to issue access token: %s',
exception,
@ -32,12 +46,9 @@ def create(request: HttpRequest,
raise
access_token = AccessTokensService().create(
account_uuid=request.user.pk,
account_uuid=account.pk,
origin=request.META['HTTP_ORIGIN'],
meta=meta,
)
request.session.pop('extension_auth_key')
request.session.save()
return access_token.key

View File

@ -13,16 +13,18 @@ from hotpocket_soa.services import AccessTokensService
LOGGER = logging.getLogger(__name__)
@register_method('accounts.auth.check')
@register_method('accounts.auth.check', namespace='accounts')
def check(request: HttpRequest) -> bool:
return request.user.is_anonymous is False
@register_method('accounts.auth.check_access_token')
@register_method('accounts.auth.check_access_token', namespace='accounts')
def check_access_token(request: HttpRequest,
access_token: str,
meta: dict | None = None,
) -> bool:
assert request.user.is_anonymous is False, 'Not authenticated'
result = True
try:

View File

@ -11,8 +11,27 @@
<div class="alert alert-success mt-3" role="alert">
<h4 class="alert-heading">{% translate 'Done!' %}</h4>
<p class="lead mb-0">
{% translate "You've successfully logged in to the extension." %}
{% if app_redirect_url %}
{% translate "You've successfully logged in to the application." %}
{% else %}
{% translate "You've successfully logged in to the extension." %}
{% endif %}
</p>
</div>
</div>
{% endblock %}
{% block page_scripts %}
{% if app_redirect_url %}
<script type="text/javascript">
(() => {
window.setTimeout(
() => {
window.location.replace('{{ app_redirect_url|safe }}');
},
1000,
);
})();
</script>
{% endif %}
{% endblock %}

View File

@ -137,6 +137,8 @@ def render_access_token_app(access_token: AccessTokenOut) -> str:
AccessTokenOriginApp.SAFARI_WEB_EXTENSION,
AccessTokenOriginApp.CHROME_EXTENSION,
AccessTokenOriginApp.FIREFOX_EXTENSION,
AccessTokenOriginApp.HOTPOCKET_DESKTOP,
AccessTokenOriginApp.HOTPOCKET_MOBILE,
)
if origin_app in extension_origin_apps:
app = UIAccessTokenOriginApp[origin_app.value].value
@ -152,7 +154,7 @@ def render_access_token_app(access_token: AccessTokenOut) -> str:
@register.filter(name='render_access_token_platform')
def render_access_token_platform(access_token: AccessTokenOut) -> str:
match access_token.meta.get('platform', None):
case 'MacIntel':
case 'MacIntel' | 'macOS':
return 'macOS'
case 'iPhone':

View File

@ -59,6 +59,13 @@ urlpatterns = [
accounts.apps.DeleteView.as_view(),
name='ui.accounts.apps.delete',
),
path(
'accounts/rpc/',
JSONRPCView.as_view(
namespace='accounts',
),
name='ui.accounts.rpc',
),
path('accounts/', accounts.index.index, name='ui.accounts.index'),
path(
'imports/pocket/',

View File

@ -2,27 +2,56 @@
from __future__ import annotations
import logging
import urllib.parse
import uuid
from django import db
from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from hotpocket_backend.apps.ui.constants import AuthSource
from hotpocket_soa.services import AuthKeysService
LOGGER = logging.getLogger(__name__)
SOURCE_TO_REDIRECT_SCHEME = {
AuthSource.DESKTOP.value: 'hotpocket-desktop',
AuthSource.MOBILE.value: 'hotpocket-mobile',
}
def authenticate(request: HttpRequest) -> HttpResponse:
if request.user.is_anonymous is False:
auth_key = str(uuid.uuid4())
source = request.GET.get(
'source',
request.session.get('extension_source', AuthSource.BROWSER_EXTENSION.value),
)
session_token = request.GET.get(
'session_token', request.session.get('extension_session_token', None),
)
request.session['extension_auth_key'] = auth_key
request.session.save()
if source == AuthSource.BROWSER_EXTENSION.value:
session_token = str(uuid.uuid4())
elif source in (AuthSource.DESKTOP.value, AuthSource.MOBILE.value):
assert session_token not in ('', None), 'Session token missing'
else:
raise ValueError(f'Unknown source: `{source}`')
request.session['extension_source'] = source
request.session['extension_session_token'] = session_token
request.session.save()
if request.user.is_anonymous is False:
with db.transaction.atomic():
auth_key = AuthKeysService().create(
account_uuid=request.user.pk,
)
return redirect(reverse(
'ui.integrations.extension.post_authenticate',
query=[
('auth_key', auth_key),
('auth_key', auth_key.key),
],
))
@ -36,12 +65,35 @@ def post_authenticate(request: HttpRequest) -> HttpResponse:
assert request.user.is_anonymous is False, 'Not authenticated'
auth_key = request.GET.get('auth_key', None)
assert request.session.get('extension_auth_key', None) == auth_key, (
'Auth key mismatch'
)
assert auth_key is not None, 'Auth key missing'
source = request.session.get('extension_source', None)
assert source is not None, 'Source is missing'
session_token = request.session.get('extension_session_token', None)
assert session_token is not None, 'Session token is missing'
app_redirect_url = None
if source in (AuthSource.DESKTOP.value, AuthSource.MOBILE.value):
app_redirect_url = urllib.parse.urlunsplit((
SOURCE_TO_REDIRECT_SCHEME[source],
'post-authenticate',
'/',
urllib.parse.urlencode([
('session_token', session_token),
('auth_key', auth_key),
]),
'',
))
request.session.pop('extension_source')
request.session.pop('extension_session_token')
request.session.save()
return render(
request, 'ui/integrations/extension/post_authenticate.html',
request,
'ui/integrations/extension/post_authenticate.html',
{
'app_redirect_url': app_redirect_url,
},
)
except AssertionError as exception:
LOGGER.error(

View File

@ -79,3 +79,5 @@ CORS_ALLOW_HEADERS = (
*default_headers,
'cookie',
)
AUTH_KEY_TTL = 30

View File

@ -1,2 +1,3 @@
from .access_token import AccessTokenFactory # noqa: F401,F403
from .account import AccountFactory # noqa: F401,F403
from .auth_key import AuthKeyFactory # noqa: F401,F403

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import uuid
import factory
from hotpocket_backend.apps.accounts.models import AuthKey
class AuthKeyFactory(factory.django.DjangoModelFactory):
account_uuid = None
key = factory.LazyFunction(lambda: str(uuid.uuid4()))
consumed_at = None
class Meta:
model = AuthKey

View File

@ -1,3 +1,4 @@
from .access_token import * # noqa: F401,F403
from .account import * # noqa: F401,F403
from .apps import * # noqa: F401,F403
from .auth_key import * # noqa: F401,F403

Some files were not shown because too many files have changed in this diff Show More