13 Commits

Author SHA1 Message Date
9f121a0ceb Release v26.1.14
Some checks failed
CI / Checks (push) Successful in 1m50s
Production deployment / Build (release) Successful in 58s
Staging deployment / Build (release) Successful in 59s
Staging deployment / Deploy (release) Successful in 1m45s
Production deployment / Deploy (release) Failing after 4m29s
2026-01-14 20:59:08 +01:00
d16f0cd957 BTHLABS-81: OG Properties on Share Association page
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2026-01-14 20:53:46 +01:00
98e5e1891a Release v25.12.04
All checks were successful
CI / Checks (push) Successful in 2m14s
Production deployment / Build (release) Successful in 26s
Staging deployment / Build (release) Successful in 50s
Staging deployment / Deploy (release) Successful in 1m19s
Production deployment / Deploy (release) Successful in 2m17s
2025-12-04 20:57:48 +01:00
06343e6ed3 BTHLABS-0000: Tweaking association card's layout 2025-12-04 20:55:55 +01:00
82a3b612ec BTHLABS-0000: Fix YT embed code 2025-12-04 20:46:38 +01:00
1e549d3fc2 Release v25.11.26
All checks were successful
Production deployment / Build (release) Successful in 32s
CI / Checks (push) Successful in 2m20s
Staging deployment / Build (release) Successful in 1m9s
Staging deployment / Deploy (release) Successful in 1m10s
Production deployment / Deploy (release) Successful in 2m25s
2025-11-27 17:52:16 +01:00
55126f4af6 BTHLABS-66: Prepping for public release: Take five
AKA "Using Apple reviewers as QA for your project". Thanks, y'all! :)
2025-11-27 17:51:19 +01:00
cca49f2292 BTHLABS-66: Prepping for public release: Take four
Apple gonna Apple... ;)
2025-11-25 12:54:52 +01:00
23f8296659 BTHLABS-66: Prepping for public release: Take three
I smell a drastic change to auth flow in the Mac app... Let's see if it
gets approved this time :D.
2025-11-21 21:04:28 +01:00
9abed01e53 BTHLABS-0000: Code cleanup 2025-11-20 20:34:42 +01:00
3b1aba9672 Release v25.11.19
Some checks failed
CI / Checks (push) Failing after 2m22s
Production deployment / Build (release) Successful in 32s
Staging deployment / Build (release) Successful in 1m26s
Production deployment / Deploy (release) Successful in 1m47s
Staging deployment / Deploy (release) Successful in 1m40s
2025-11-19 20:31:02 +01:00
22061486a8 BTHLABS-66: Prepping for public release: Take two 2025-11-19 20:28:31 +01:00
38785ccf92 BTHLABS-0000: Fixing Share extension builds in Release configs 2025-11-19 07:19:19 +01:00
61 changed files with 881 additions and 191 deletions

View File

@@ -1,6 +1,23 @@
# HotPocket by BTHLabs # HotPocket by BTHLabs
This repository contains the _HotPocket_ project. Minimal self-hosted bookmarking app :).
## The what, the why and the ugly
HotPocket is a minimal self-hosted bookmarking app. It combines a Web
application, companion apps, browser extensions to give you a way to quickly
save links for later.
HotPocket came to be to fill in the blank left by Pocket, after Mozilla shut it
down. I looked at the existing alternatives and found them either too
feature-rich, too involved to self-host or otherwise not to my liking. So I
decided to sit down and build something for myself.
With the what and why out of the way, let's talk about the ugly... At its core
HotPocket is a personal project. I built it by myself and for myself. It may
or may not fit your needs. If it does, happy saving!
If you're feeling up for an adventure, continue reading below :).
## Development setup ## Development setup
@@ -66,7 +83,7 @@ $ docker run --rm -it \
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME=hotpocket \ -e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME=hotpocket \
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD=hotpocketm4st3r \ -e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD=hotpocketm4st3r \
-p 8000:8000 \ -p 8000:8000 \
hotpocket/backend:aio-v25.11.18-01 hotpocket/backend:aio-v25.11.26-01
``` ```
The command above will set up and start the application. The SQLite file will The command above will set up and start the application. The SQLite file will
@@ -128,6 +145,7 @@ that can be used to configure the services.
| `HOTPOCKET_BACKEND_RUN_MIGRATIONS` | `false` or `true` | Set to `true` to run database muigrations when the container starts. | | `HOTPOCKET_BACKEND_RUN_MIGRATIONS` | `false` or `true` | Set to `true` to run database muigrations when the container starts. |
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME` | N/A | Username for the initial account. | | `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME` | N/A | Username for the initial account. |
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD` | N/A | Password for the initial account. | | `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD` | N/A | Password for the initial account. |
| `HOTPOCKET_BACKEND_OPERATOR_EMAIL` | N/A | Instance operator's e-mail. Used to display extra language on login page. |
**Env and App settings** **Env and App settings**
@@ -158,6 +176,15 @@ method's name in the UI and defaults to `OIDC`.
**NOTE:** Currently, only Keycloak has been tested with this login method. **NOTE:** Currently, only Keycloak has been tested with this login method.
### Volumes
Both images declare `/srv/run` to be a volume. It's intended to keep the
service's runtime data, including but not limited to PID files, UNIX sockets
etc. It's recommended to persist this volume.
Additionally, the `deployment` image declares `/srv/uploads` to be a volume.
It's recommeded to persist this volume.
## Author ## Author
_HotPocket_ is developed by [BTHLabs](https://www.bthlabs.pl/). _HotPocket_ is developed by [BTHLabs](https://www.bthlabs.pl/).

View File

@@ -1,6 +1,6 @@
services: services:
backend: backend:
image: "hotpocket/backend:aio-v25.11.18-01" image: "bthlabs/hotpocket:aio-v25.11.26-01"
environment: environment:
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright" HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket" HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"

View File

@@ -8,7 +8,7 @@ x-backend-environment: &x-backend-environment
services: services:
webapp: webapp:
image: "hotpocket/backend:deployment-v25.11.18-01" image: "bthlabs/hotpocket:deployment-v25.11.26-01"
environment: environment:
<<: *x-backend-environment <<: *x-backend-environment
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net" HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
@@ -21,7 +21,7 @@ services:
restart: "unless-stopped" restart: "unless-stopped"
admin: admin:
image: "hotpocket/backend:deployment-v25.11.18-01" image: "bthlabs/hotpocket:deployment-v25.11.26-01"
environment: environment:
<<: *x-backend-environment <<: *x-backend-environment
HOTPOCKET_BACKEND_APP: "admin" HOTPOCKET_BACKEND_APP: "admin"
@@ -35,7 +35,7 @@ services:
restart: "unless-stopped" restart: "unless-stopped"
celery-worker: celery-worker:
image: "hotpocket/backend:deployment-v25.11.18-01" image: "bthlabs/hotpocket:deployment-v25.11.26-01"
command: command:
- "/srv/venv/bin/celery" - "/srv/venv/bin/celery"
- "-A" - "-A"
@@ -57,7 +57,7 @@ services:
restart: "unless-stopped" restart: "unless-stopped"
celery-beat: celery-beat:
image: "hotpocket/backend:deployment-v25.11.18-01" image: "bthlabs/hotpocket:deployment-v25.11.26-01"
command: command:
- "/srv/venv/bin/celery" - "/srv/venv/bin/celery"
- "-A" - "-A"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "hotpocket-workspace" name = "hotpocket-workspace"
version = "25.11.18" version = "25.11.26"
description = "HotPocket Workspace" description = "HotPocket Workspace"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"] authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0" license = "Apache-2.0"

View File

@@ -7,8 +7,8 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
4C1159202E8B055F003B34AD /* Save to HotPocket Development.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4CBCEA4F2E81CB9500722009 /* Save to HotPocket Development.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4C1159202E8B055F003B34AD /* Save to HotPocket.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4CBCEA4F2E81CB9500722009 /* Save to HotPocket.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4C2F0C692E851BBD0033F5C2 /* Save to HotPocket Development.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket Development.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 */; }; 4C70F30D2E8869FB00320048 /* HPShareExtensionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C70F30C2E8869FB00320048 /* HPShareExtensionHelper.m */; };
4C70F30E2E8869FB00320048 /* 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 */; }; 4C70F3152E886A8F00320048 /* HPSharedItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C70F3142E886A8F00320048 /* HPSharedItem.m */; };
@@ -58,7 +58,7 @@
dstSubfolderSpec = 13; dstSubfolderSpec = 13;
files = ( files = (
4CABCAD62E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */, 4CABCAD62E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */,
4C2F0C692E851BBD0033F5C2 /* Save to HotPocket Development.appex in Embed Foundation Extensions */, 4C2F0C692E851BBD0033F5C2 /* Save to HotPocket.appex in Embed Foundation Extensions */,
); );
name = "Embed Foundation Extensions"; name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -69,7 +69,7 @@
dstPath = ""; dstPath = "";
dstSubfolderSpec = 13; dstSubfolderSpec = 13;
files = ( files = (
4C1159202E8B055F003B34AD /* Save to HotPocket Development.appex in Embed Foundation Extensions */, 4C1159202E8B055F003B34AD /* Save to HotPocket.appex in Embed Foundation Extensions */,
4CABCAE02E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */, 4CABCAE02E56F0C900D8A354 /* HotPocket Extension.appex in Embed Foundation Extensions */,
); );
name = "Embed Foundation Extensions"; name = "Embed Foundation Extensions";
@@ -78,7 +78,7 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket Development.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Save to HotPocket Development.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 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>"; }; 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>"; }; 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>"; }; 4C70F3132E886A8F00320048 /* HPSharedItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HPSharedItem.h; sourceTree = "<group>"; };
@@ -89,7 +89,7 @@
4CABCAC62E56F0C900D8A354 /* HotPocket Development.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "HotPocket Development.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4CABCAC62E56F0C900D8A354 /* HotPocket Development.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "HotPocket Development.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; }; 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; }; 4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "HotPocket Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
4CBCEA4F2E81CB9500722009 /* Save to HotPocket Development.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Save to HotPocket Development.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 */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -393,8 +393,8 @@
4CABCAC62E56F0C900D8A354 /* HotPocket Development.app */, 4CABCAC62E56F0C900D8A354 /* HotPocket Development.app */,
4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */, 4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */,
4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */, 4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */,
4CBCEA4F2E81CB9500722009 /* Save to HotPocket Development.appex */, 4CBCEA4F2E81CB9500722009 /* Save to HotPocket.appex */,
4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket Development.appex */, 4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket.appex */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -421,7 +421,7 @@
packageProductDependencies = ( packageProductDependencies = (
); );
productName = "iOS (Share Extension)"; productName = "iOS (Share Extension)";
productReference = 4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket Development.appex */; productReference = 4C2F0C5E2E851BBD0033F5C2 /* Save to HotPocket.appex */;
productType = "com.apple.product-type.app-extension"; productType = "com.apple.product-type.app-extension";
}; };
4CABCAAF2E56F0C900D8A354 /* HotPocket (iOS) */ = { 4CABCAAF2E56F0C900D8A354 /* HotPocket (iOS) */ = {
@@ -537,7 +537,7 @@
packageProductDependencies = ( packageProductDependencies = (
); );
productName = "macOS (Share Extension)"; productName = "macOS (Share Extension)";
productReference = 4CBCEA4F2E81CB9500722009 /* Save to HotPocket Development.appex */; productReference = 4CBCEA4F2E81CB9500722009 /* Save to HotPocket.appex */;
productType = "com.apple.product-type.app-extension"; productType = "com.apple.product-type.app-extension";
}; };
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
@@ -718,7 +718,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements"; CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist"; INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
@@ -731,9 +731,9 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket Development"; PRODUCT_NAME = "Save to HotPocket";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -751,7 +751,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements"; CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist"; INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
@@ -764,7 +764,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket"; PRODUCT_NAME = "Save to HotPocket";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -784,7 +784,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist"; INFOPLIST_FILE = "iOS (Extension)/Info.plist";
@@ -797,7 +797,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -819,7 +819,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist"; INFOPLIST_FILE = "iOS (Extension)/Info.plist";
@@ -832,7 +832,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -858,7 +858,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (App)/Info.plist"; INFOPLIST_FILE = "iOS (App)/Info.plist";
@@ -878,7 +878,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -904,7 +904,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (App)/Info.plist"; INFOPLIST_FILE = "iOS (App)/Info.plist";
@@ -924,7 +924,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -950,7 +950,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements"; CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -965,7 +965,7 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -985,7 +985,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements"; CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -1000,7 +1000,7 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -1022,7 +1022,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements"; CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1038,7 +1038,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -1061,7 +1061,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements"; CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1077,7 +1077,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-framework", "-framework",
SafariServices, SafariServices,
@@ -1211,7 +1211,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements"; CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1225,9 +1225,9 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket Development"; PRODUCT_NAME = "Save to HotPocket";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
SDKROOT = macosx; SDKROOT = macosx;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -1241,7 +1241,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements"; CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2025111801; CURRENT_PROJECT_VERSION = 2025112601;
DEVELOPMENT_TEAM = 648728X64K; DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1255,7 +1255,7 @@
"@executable_path/../../../../Frameworks", "@executable_path/../../../../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.18; MARKETING_VERSION = 25.11.26;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket"; PRODUCT_NAME = "Save to HotPocket";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;

View File

@@ -10,6 +10,7 @@
#import "HPAPI.h" #import "HPAPI.h"
#import "HPCredentialsHelper.h" #import "HPCredentialsHelper.h"
#import "HPRPCClient.h" #import "HPRPCClient.h"
#import "NSBundle+HotPocketExtensions.h"
#import "NSURL+HotPocketExtensions.h" #import "NSURL+HotPocketExtensions.h"
@implementation HPAuthParams @implementation HPAuthParams
@@ -77,18 +78,19 @@
return nil; return nil;
} }
NSDictionary *postAuthenticateURLParams = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"HPAuthFlowPostAuthenticateURLParts"]; NSString *expectedScheme = [NSBundle postAuthenticateURLScheme];
if (postAuthenticateURLParams == nil) { NSString *expectedHost = [NSBundle postAuthenticateURLHost];
if (expectedScheme == nil || expectedHost == nil) {
return nil; return nil;
} }
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
if ([urlComponents.scheme isEqualToString:[postAuthenticateURLParams valueForKey:@"scheme"]] == NO) { if ([urlComponents.scheme isEqualToString:expectedScheme] == NO) {
return nil; return nil;
} }
if ([urlComponents.host isEqualToString:[postAuthenticateURLParams valueForKey:@"host"]] == NO) { if ([urlComponents.host isEqualToString:expectedHost] == NO) {
return nil; return nil;
} }
@@ -109,6 +111,8 @@
} }
-(BOOL)handleAuthParams:(HPAuthParams *)authParams { -(BOOL)handleAuthParams:(HPAuthParams *)authParams {
[[NSNotificationCenter defaultCenter] postNotificationName:@"AuthFlowDidReceiveAuthParams" object:self];
HPRPCClient *rpcClient = [[HPRPCClient alloc] initWithBaseURL:self.baseURL accessToken:nil]; HPRPCClient *rpcClient = [[HPRPCClient alloc] initWithBaseURL:self.baseURL accessToken:nil];
NSArray *callParams = @[ NSArray *callParams = @[
@@ -120,7 +124,7 @@
method:@"accounts.access_tokens.create" method:@"accounts.access_tokens.create"
params:callParams endopoint:@"/accounts/rpc/" params:callParams endopoint:@"/accounts/rpc/"
completionHandler:^(NSString *callId, HPRPCCallResult *result) { completionHandler:^(NSString *callId, HPRPCCallResult *result) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (result.error != nil) { if (result.error != nil) {
NSLog(@"-[HPAuthFlow handleAuthParams:] error=`%@`", result.error); NSLog(@"-[HPAuthFlow handleAuthParams:] error=`%@`", result.error);
} else { } else {

View File

@@ -12,6 +12,9 @@ NS_ASSUME_NONNULL_BEGIN
@interface NSBundle (HotPocketExtensions) @interface NSBundle (HotPocketExtensions)
+(NSString *)uname; +(NSString *)uname;
+(NSDictionary *)postAuthenticateURLParams;
+(NSString *)postAuthenticateURLScheme;
+(NSString *)postAuthenticateURLHost;
@end @end

View File

@@ -14,4 +14,23 @@
return [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]]; return [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]];
} }
+(NSDictionary *)postAuthenticateURLParams {
NSDictionary *result = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"HPAuthFlowPostAuthenticateURLParts"];
if (result == nil) {
return [NSDictionary dictionary];
}
return result;
}
+(NSString *)postAuthenticateURLScheme {
NSDictionary *params = [self postAuthenticateURLParams];
return [params valueForKey:@"scheme"];
}
+(NSString *)postAuthenticateURLHost {
NSDictionary *params = [self postAuthenticateURLParams];
return [params valueForKey:@"host"];
}
@end @end

View File

@@ -6,12 +6,19 @@
// //
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <AuthenticationServices/AuthenticationServices.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@interface AuthorizationProgressViewController : UIViewController @class MultilineLabel;
@interface AuthorizationProgressViewController : UIViewController<ASWebAuthenticationPresentationContextProviding>
@property IBOutlet UIActivityIndicatorView *progressIndicator; @property IBOutlet UIActivityIndicatorView *progressIndicator;
@property IBOutlet MultilineLabel *progressLabel;
@property (strong, nullable) NSURL *authorizationURL;
@property (strong, nullable) ASWebAuthenticationSession *webAuthenticationSession;
@property BOOL userCancelledSession;
@end @end

View File

@@ -8,20 +8,36 @@
#import "AuthorizationProgressViewController.h" #import "AuthorizationProgressViewController.h"
#import "AppDelegate.h" #import "AppDelegate.h"
#import "HPAuthFlow.h"
#import "HPCredentialsHelper.h" #import "HPCredentialsHelper.h"
#import "MultilineLabel.h"
#import "NSBundle+HotPocketExtensions.h"
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate) @interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
#pragma mark - Private interface #pragma mark - Private interface
-(void)presentAuthorizationError;
@end @end
@implementation AuthorizationProgressViewController @implementation AuthorizationProgressViewController
#pragma mark - View lifecycle #pragma mark - View lifecycle
-(instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
self.authorizationURL = nil;
self.webAuthenticationSession = nil;
self.userCancelledSession = NO;
}
return self;
}
-(void)viewDidLoad { -(void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
self.progressLabel.text = NSLocalizedString(@"Continue to sign in in your browser...", @"Continue to sign in in your browser...");
} }
-(void)viewWillAppear:(BOOL)animated { -(void)viewWillAppear:(BOOL)animated {
@@ -29,17 +45,90 @@
[self.progressIndicator startAnimating]; [self.progressIndicator startAnimating];
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate]; AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAuthFlowDidFinish:) name:@"AuthFlowDidFinish" object:appDelegate.authFlow]; [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAuthFlowDidFinish:)
name:@"AuthFlowDidFinish"
object:appDelegate.authFlow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAuthFlowDidReceiveAuthParams:)
name:@"AuthFlowDidReceiveAuthParams"
object:appDelegate.authFlow];
} }
-(void)viewWillDisappear:(BOOL)animated { -(void)viewDidAppear:(BOOL)animated {
[super viewWillDisappear:animated]; [super viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self]; AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
ASWebAuthenticationSessionCompletionHandler completionHandler = ^(NSURL *url, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil) {
#ifdef DEBUG
NSLog(@"[AuthorizationViewController.session completionHandler] error=`%@`", error);
#endif
if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
self.userCancelledSession = YES;
}
[self presentAuthorizationError];
} else {
HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url];
if (receivedAuthParams != nil) {
[appDelegate.authFlow handleAuthParams:receivedAuthParams];
} else {
[self presentAuthorizationError];
}
}
self.webAuthenticationSession = nil;
});
};
ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithCustomScheme:[NSBundle postAuthenticateURLScheme]];
self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:self.authorizationURL
callback:callback
completionHandler:completionHandler];
self.webAuthenticationSession.presentationContextProvider = self;
#ifdef DEBUG
self.webAuthenticationSession.prefersEphemeralWebBrowserSession = YES;
#endif
if (self.webAuthenticationSession.canStart == NO) {
[self presentAuthorizationError];
return;
}
[self.webAuthenticationSession start];
} }
-(void)viewDidDisappear:(BOOL)animated { -(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated]; [super viewDidDisappear:animated];
self.webAuthenticationSession = nil;
[self.progressIndicator stopAnimating]; [self.progressIndicator stopAnimating];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
# pragma mark - Private interface
-(void)presentAuthorizationError {
if (self.userCancelledSession == 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 popViewControllerAnimated:YES];
}
} }
#pragma mark - Notification handlers #pragma mark - Notification handlers
@@ -52,23 +141,21 @@
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials]; HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
if (credentials.usable == NO) { if (credentials.usable == NO) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Oops!", @"Oops!") [self presentAuthorizationError];
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 { } else {
[self.navigationController popToRootViewControllerAnimated:YES]; [self.navigationController popToRootViewControllerAnimated:YES];
} }
}); });
} }
-(void)onAuthFlowDidReceiveAuthParams:(NSNotification *)notification {
self.progressLabel.text = NSLocalizedString(@"Processing authorization...", @"Processing authorization...");
}
# pragma mark - ASWebAuthenticationPresentationContextProviding implementation
-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
return self.view.window;
}
@end @end

View File

@@ -73,14 +73,9 @@
return; return;
} }
if ([application canOpenURL:authURL] == YES) { AuthorizationProgressViewController *authorizationProgressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationProgressViewController"];
[application openURL:authURL options:@{} completionHandler:^(BOOL result) { authorizationProgressViewController.authorizationURL = authURL;
if (result == YES) { [self.navigationController pushViewController:authorizationProgressViewController animated:YES];
AuthorizationProgressViewController *authorizationProgressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationProgressViewController"];
[self.navigationController pushViewController:authorizationProgressViewController animated:YES];
}
}];
}
} }
#pragma mark - Event handlers #pragma mark - Event handlers

View File

@@ -229,8 +229,8 @@
<rect key="frame" x="189" y="306" width="37" height="37"/> <rect key="frame" x="189" y="306" width="37" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView> </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"> <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" customClass="MultilineLabel">
<rect key="frame" x="20" y="359" width="374" height="21"/> <rect key="frame" x="20" y="359" width="374" height="64"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -242,6 +242,7 @@
</view> </view>
<connections> <connections>
<outlet property="progressIndicator" destination="DNy-gf-n60" id="hJF-jc-ZJ0"/> <outlet property="progressIndicator" destination="DNy-gf-n60" id="hJF-jc-ZJ0"/>
<outlet property="progressLabel" destination="qiJ-yx-nMd" id="1Wu-em-XsK"/>
</connections> </connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="N3D-cM-5Ro" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="N3D-cM-5Ro" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>

View File

@@ -21,18 +21,6 @@
} }
-(void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls { -(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 @end

View File

@@ -6,12 +6,19 @@
// //
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <AuthenticationServices/AuthenticationServices.h>
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@interface AuthorizationProgressViewController : NSViewController @interface AuthorizationProgressViewController : NSViewController<ASWebAuthenticationPresentationContextProviding>
@property IBOutlet NSProgressIndicator *progressIndicator; @property IBOutlet NSProgressIndicator *progressIndicator;
@property NSString *progressLabelTitle;
@property (nullable, strong) NSURL *authorizationURL;
@property (nullable, strong) ASWebAuthenticationSession *webAuthenticationSession;
@property BOOL userCancelledSession;
-(IBAction)doCancel:(id)sender;
@end @end

View File

@@ -9,37 +9,133 @@
#import "AppDelegate.h" #import "AppDelegate.h"
#import "AuthorizationViewController.h" #import "AuthorizationViewController.h"
#import "HPAuthFlow.h"
#import "HPCredentialsHelper.h" #import "HPCredentialsHelper.h"
#import "MainViewController.h" #import "MainViewController.h"
#import "NSBundle+HotPocketExtensions.h"
#import "ReplaceAnimator.h" #import "ReplaceAnimator.h"
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate) @interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
#pragma mark - Private interface #pragma mark - Private interface
-(void)presentAuthorizationError;
@end @end
@implementation AuthorizationProgressViewController @implementation AuthorizationProgressViewController
#pragma mark - View lifecycle #pragma mark - View lifecycle
-(instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
self.authorizationURL = nil;
self.webAuthenticationSession = nil;
self.userCancelledSession = NO;
}
return self;
}
-(void)viewDidLoad { -(void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
self.progressLabelTitle = NSLocalizedString(@"Continue to sign in in your browser...", @"Continue to sign in in your browser...");
} }
-(void)viewWillAppear { -(void)viewWillAppear {
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate]; AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAuthFlowDidFinish:) name:@"AuthFlowDidFinish" object:appDelegate.authFlow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAuthFlowDidFinish:)
name:@"AuthFlowDidFinish"
object:appDelegate.authFlow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAuthFlowDidReceiveAuthParams:)
name:@"AuthFlowDidReceiveAuthParams"
object:appDelegate.authFlow];
[self.progressIndicator startAnimation:self]; [self.progressIndicator startAnimation:self];
} }
-(void)viewDidAppear {
[super viewDidAppear];
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
ASWebAuthenticationSessionCompletionHandler completionHandler = ^(NSURL *url, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil) {
#ifdef DEBUG
NSLog(@"[AuthorizationViewController.session completionHandler] error=`%@`", error);
#endif
if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
self.userCancelledSession = YES;
}
[self presentAuthorizationError];
} else {
HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url];
if (receivedAuthParams != nil) {
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
[appDelegate.authFlow handleAuthParams:receivedAuthParams];
} else {
[self presentAuthorizationError];
}
}
self.webAuthenticationSession = nil;
});
};
ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithCustomScheme:[NSBundle postAuthenticateURLScheme]];
self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:self.authorizationURL
callback:callback
completionHandler:completionHandler];
self.webAuthenticationSession.presentationContextProvider = self;
#ifdef DEBUG
self.webAuthenticationSession.prefersEphemeralWebBrowserSession = YES;
#endif
if (self.webAuthenticationSession.canStart == NO) {
[self presentAuthorizationError];
return;
}
[self.webAuthenticationSession start];
}
-(void)viewDidDisappear { -(void)viewDidDisappear {
[super viewDidDisappear];
self.webAuthenticationSession = nil;
[self.progressIndicator stopAnimation:self]; [self.progressIndicator stopAnimation:self];
[[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self];
} }
#pragma mark - Actions
-(IBAction)doCancel:(id)sender {
[self.webAuthenticationSession cancel];
}
#pragma mark - Private interface
-(void)presentAuthorizationError {
if (self.userCancelledSession == 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 beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse response) {
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
}];
} else {
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
}
}
#pragma mark - Notification handlers #pragma mark - Notification handlers
-(void)onAuthFlowDidFinish:(NSNotification *)notification { -(void)onAuthFlowDidFinish:(NSNotification *)notification {
@@ -50,14 +146,7 @@
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
if (credentials.usable == NO) { if (credentials.usable == NO) {
NSAlert *alert = [[NSAlert alloc] init]; [self presentAuthorizationError];
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 { } else {
MainViewController *mainViewController = [self.storyboard instantiateControllerWithIdentifier:@"MainViewController"]; MainViewController *mainViewController = [self.storyboard instantiateControllerWithIdentifier:@"MainViewController"];
[self presentViewController:mainViewController animator:[[ReplaceAnimator alloc] init]]; [self presentViewController:mainViewController animator:[[ReplaceAnimator alloc] init]];
@@ -65,4 +154,14 @@
}); });
} }
-(void)onAuthFlowDidReceiveAuthParams:(NSNotification *)notification {
self.progressLabelTitle = NSLocalizedString(@"Processing authorization...", @"Processing authorization...");
}
# pragma mark - ASWebAuthenticationPresentationContextProviding implementation
-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
return self.view.window;
}
@end @end

View File

@@ -31,19 +31,24 @@
#pragma mark - Actions #pragma mark - Actions
-(IBAction)doStartAuthorizationFlow:(id)sender { -(IBAction)doStartAuthorizationFlow:(id)sender {
AppDelegate *appDeleate = [[NSApplication sharedApplication] delegate]; AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
appDeleate.authFlow.baseURL = [NSURL URLWithString:self.baseURL]; appDelegate.authFlow.baseURL = [NSURL URLWithString:self.baseURL];
NSURL *authURL = [appDeleate.authFlow start]; NSURL *authURL = [appDelegate.authFlow start];
if (authURL == nil) { if (authURL == nil) {
NSBeep(); NSBeep();
return; return;
} }
AuthorizationProgressViewController *authProgressViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationProgressViewController"];
[self presentViewController:authProgressViewController animator:[[ReplaceAnimator alloc] init]];
[[NSWorkspace sharedWorkspace] openURL:authURL]; AuthorizationProgressViewController *authProgressViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationProgressViewController"];
authProgressViewController.authorizationURL = authURL;
[self presentViewController:authProgressViewController animator:[[ReplaceAnimator alloc] init]];
}
# pragma mark - ASWebAuthenticationPresentationContextProviding implementation
-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
return self.view.window;
} }
@end @end

View File

@@ -103,7 +103,7 @@
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/> <rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7sM-F3-Zzf"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7sM-F3-Zzf">
<rect key="frame" x="18" y="153" width="389" height="16"/> <rect key="frame" x="18" y="153" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="HotPocket Instance URL" id="XwM-DV-kei"> <textFieldCell key="cell" lineBreakMode="clipping" title="HotPocket Instance URL" id="XwM-DV-kei">
@@ -112,7 +112,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ygC-xe-m6y"> <textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ygC-xe-m6y">
<rect key="frame" x="20" y="124" width="385" height="21"/> <rect key="frame" x="20" y="124" width="385" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <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"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" id="rHK-hP-yWO">
@@ -128,7 +128,7 @@
</binding> </binding>
</connections> </connections>
</textField> </textField>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DIc-8O-uoQ"> <textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DIc-8O-uoQ">
<rect key="frame" x="18" y="68" width="389" height="48"/> <rect key="frame" x="18" y="68" width="389" height="48"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" title="Enter the URL to your HotPocket instance, e.g. https://hotpocket.yourcompany.com/" id="Y0q-a1-oBP"> <textFieldCell key="cell" selectable="YES" title="Enter the URL to your HotPocket instance, e.g. https://hotpocket.yourcompany.com/" id="Y0q-a1-oBP">
@@ -154,7 +154,7 @@
<action selector="doStartAuthorizationFlow:" target="XfG-lQ-9wD" id="AOi-Wt-gmL"/> <action selector="doStartAuthorizationFlow:" target="XfG-lQ-9wD" id="AOi-Wt-gmL"/>
</connections> </connections>
</button> </button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mQc-Ea-NNN"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mQc-Ea-NNN">
<rect key="frame" x="18" y="185" width="389" height="28"/> <rect key="frame" x="18" y="185" width="389" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="NTZ-zl-yhk"> <textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="NTZ-zl-yhk">
@@ -177,7 +177,7 @@
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/> <rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yRj-hC-QYS"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yRj-hC-QYS">
<rect key="frame" x="18" y="185" width="389" height="28"/> <rect key="frame" x="18" y="185" width="389" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="F4l-2Z-D79"> <textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="F4l-2Z-D79">
@@ -195,15 +195,32 @@
<rect key="frame" x="196" y="113" width="32" height="32"/> <rect key="frame" x="196" y="113" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator> </progressIndicator>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g9a-gR-c7o"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g9a-gR-c7o">
<rect key="frame" x="18" y="81" width="389" height="16"/> <rect key="frame" x="18" y="49" width="389" height="48"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Awaiting authorization response..." id="3oi-LK-vKv"> <textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Awaiting authorization response..." id="3oi-LK-vKv">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
<connections>
<binding destination="OX4-Oj-1cw" name="value" keyPath="self.progressLabelTitle" id="ydU-jy-p3F"/>
</connections>
</textField> </textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Sip-bU-V5i">
<rect key="frame" x="175" y="14" 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="nYJ-LO-V7O">
<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="doCancel:" target="OX4-Oj-1cw" id="nAC-If-aib"/>
</connections>
</button>
</subviews> </subviews>
</view> </view>
<connections> <connections>
@@ -227,7 +244,7 @@
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/> <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"/> <imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyDown" image="icon-mac-384" id="fae-mz-0sj"/>
</imageView> </imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T7q-KB-3Ut"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T7q-KB-3Ut">
<rect key="frame" x="18" y="185" width="389" height="28"/> <rect key="frame" x="18" y="185" width="389" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="r5O-Sk-IdK"> <textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="r5O-Sk-IdK">
@@ -236,7 +253,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2h7-bN-dsa"> <textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2h7-bN-dsa">
<rect key="frame" x="18" y="153" width="389" height="16"/> <rect key="frame" x="18" y="153" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" title="HotPocket is configured and ready." id="5fh-mh-WR1"> <textFieldCell key="cell" selectable="YES" title="HotPocket is configured and ready." id="5fh-mh-WR1">
@@ -245,7 +262,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uci-UC-wxo"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uci-UC-wxo">
<rect key="frame" x="18" y="89" width="389" height="16"/> <rect key="frame" x="18" y="89" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Instance URL" id="azk-ea-KeN"> <textFieldCell key="cell" lineBreakMode="clipping" title="Instance URL" id="azk-ea-KeN">
@@ -267,8 +284,8 @@
<binding destination="r5D-xE-cNT" name="enabled" keyPath="self.logoutButtonEnabled" id="gTs-BO-USz"/> <binding destination="r5D-xE-cNT" name="enabled" keyPath="self.logoutButtonEnabled" id="gTs-BO-USz"/>
</connections> </connections>
</button> </button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8H3-oU-acU" customClass="LinkLabel"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8H3-oU-acU" customClass="LinkLabel">
<rect key="frame" x="18" y="65" width="389" height="16"/> <rect key="frame" x="18" y="69" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" allowsEditingTextAttributes="YES" id="EoA-mM-phM"> <textFieldCell key="cell" lineBreakMode="clipping" allowsEditingTextAttributes="YES" id="EoA-mM-phM">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@@ -276,7 +293,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9pl-Ap-yxc"> <textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9pl-Ap-yxc">
<rect key="frame" x="18" y="113" width="389" height="32"/> <rect key="frame" x="18" y="113" width="389" height="32"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" title="Safari and Share Extensions are installed." id="dy7-bw-DYh"> <textFieldCell key="cell" selectable="YES" title="Safari and Share Extensions are installed." id="dy7-bw-DYh">

View File

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

View File

@@ -27,7 +27,7 @@ COPY --chown=$APP_USER_UID:$APP_USER_GID packages/common/ /srv/packages/common/
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/soa/ /srv/packages/soa/ COPY --chown=$APP_USER_UID:$APP_USER_GID packages/soa/ /srv/packages/soa/
RUN poetry install --only main,deployment && \ RUN poetry install --only main,deployment && \
minify -i --css-precision 0 --js-precision 0 --js-version 2022 hotpocket_backend/apps/ui/static/ui/css/hotpocket-backend*.css hotpocket_backend/apps/ui/static/ui/js/hotpocket*.js && \ minify -i --css-precision 0 --js-precision 0 --js-version 2022 hotpocket_backend/apps/ui/static/ui/css/hotpocket-backend*.css hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend*.js && \
./manage.py collectstatic --settings hotpocket_backend.settings.deployment.build --noinput && \ ./manage.py collectstatic --settings hotpocket_backend.settings.deployment.build --noinput && \
find hotpocket_backend/static/ -name "*.map*" -delete && \ find hotpocket_backend/static/ -name "*.map*" -delete && \
rm -f hotpocket_backend/settings/deployment/build.py && \ rm -f hotpocket_backend/settings/deployment/build.py && \

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
version = '25.11.18' version = '26.1.14'

View File

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

View File

@@ -2,16 +2,41 @@
from __future__ import annotations from __future__ import annotations
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.accounts.models import AccessToken from hotpocket_backend.apps.accounts.models import AccessToken
class AccessTokenAdmin(admin.ModelAdmin): class AccessTokenAdmin(admin.ModelAdmin):
list_display = ('pk', 'account_uuid', 'origin', 'created_at', 'is_active') list_display = (
'pk', 'account_uuid', 'origin', 'created_at', 'render_is_active',
)
search_fields = ('pk', 'account_uuid', 'key', 'origin') search_fields = ('pk', 'account_uuid', 'key', 'origin')
readonly_fields = (
'pk',
'account_uuid',
'key',
'origin',
'meta',
'created_at',
'deleted_at',
)
ordering = ['-created_at']
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return request.user.is_superuser return request.user.is_superuser
@admin.display(
description=_('Is Active?'), boolean=True, ordering='-deleted_at',
)
def render_is_active(self, obj: AccessToken | None = None) -> bool | None:
if obj is None:
return None
return obj.is_active
admin.site.register(AccessToken, AccessTokenAdmin) admin.site.register(AccessToken, AccessTokenAdmin)

View File

@@ -3,15 +3,26 @@ from __future__ import annotations
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.accounts.models import Account from hotpocket_backend.apps.accounts.models import Account
class AccountAdmin(UserAdmin): class AccountAdmin(UserAdmin):
list_display = (*UserAdmin.list_display, 'is_active') list_display = (*UserAdmin.list_display, 'render_is_active')
ordering = ['username']
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return request.user.is_superuser return request.user.is_superuser
@admin.display(
description=_('Is Active?'), boolean=True, ordering='-is_active',
)
def render_is_active(self, obj: Account | None = None) -> bool | None:
if obj is None:
return None
return obj.is_active
admin.site.register(Account, AccountAdmin) admin.site.register(Account, AccountAdmin)

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.accounts.models import AuthKey
class AuthKeyAdmin(admin.ModelAdmin):
list_display = (
'pk', 'account_uuid', 'key', 'created_at', 'consumed_at', 'render_is_active',
)
search_fields = ('pk', 'account_uuid', 'key')
readonly_fields = (
'pk',
'account_uuid',
'key',
'consumed_at',
'created_at',
'deleted_at',
)
ordering = ['-created_at']
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
@admin.display(
description=_('Is Active?'), boolean=True, ordering='-deleted_at',
)
def render_is_active(self, obj: AuthKey | None = None) -> bool | None:
if obj is None:
return None
return obj.is_active
admin.site.register(AuthKey, AuthKeyAdmin)

View File

@@ -38,3 +38,5 @@ class PSettings(typing.Protocol):
UI_PAGE_HEAD_INCLUDES: list UI_PAGE_HEAD_INCLUDES: list
UI_PAGE_SCRIPT_INCLUDES: list UI_PAGE_SCRIPT_INCLUDES: list
OPERATOR_EMAIL: str | None

View File

@@ -1 +1,2 @@
from . import association # noqa: F401
from . import save # noqa: F401 from . import save # noqa: F401

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.saves.models import Association
class AssociationAdmin(admin.ModelAdmin):
list_display = (
'pk', 'account_uuid', 'target', 'created_at', 'render_is_active',
)
search_fields = ('pk', 'account_uuid')
fields = (
'pk',
'account_uuid',
'archived_at',
'starred_at',
'target_meta',
'target_title',
'target_description',
'created_at',
'deleted_at',
)
readonly_fields = (
'pk',
'account_uuid',
'target_meta',
'target',
'archived_at',
'starred_at',
'created_at',
'deleted_at',
)
ordering = ['-created_at']
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
@admin.display(
description=_('Is Active?'), boolean=True, ordering='-deleted_at',
)
def render_is_active(self, obj: Association | None = None) -> bool | None:
if obj is None:
return None
return obj.is_active
admin.site.register(Association, AssociationAdmin)

View File

@@ -11,6 +11,26 @@ class SaveAdmin(admin.ModelAdmin):
list_display = ( list_display = (
'pk', 'key', 'account_uuid', 'created_at', 'render_is_active', 'pk', 'key', 'account_uuid', 'created_at', 'render_is_active',
) )
search_fields = ('pk', 'account_uuid', 'url')
fields = (
'pk',
'account_uuid',
'key',
'url',
'title',
'description',
'last_processed_at',
'is_netloc_banned',
'created_at',
'deleted_at',
)
readonly_fields = (
'pk',
'account_uuid',
'key',
'created_at',
'deleted_at',
)
ordering = ['-created_at'] ordering = ['-created_at']
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):

View File

@@ -100,3 +100,9 @@ def page_includes(request: HttpRequest) -> dict:
'script': settings.UI_PAGE_SCRIPT_INCLUDES, 'script': settings.UI_PAGE_SCRIPT_INCLUDES,
}, },
} }
def operator_email(request: HttpRequest) -> dict:
return {
'OPERATOR_EMAIL': settings.OPERATOR_EMAIL,
}

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import uuid
import pydantic
class SavesCreateOut(pydantic.BaseModel):
id: uuid.UUID
target_uuid: uuid.UUID
url: pydantic.AnyHttpUrl
def to_rpc(self) -> dict:
return {
'id': self.id,
'target_uuid': self.target_uuid,
'url': str(self.url),
}

View File

@@ -3,19 +3,28 @@ from __future__ import annotations
from bthlabs_jsonrpc_core import register_method from bthlabs_jsonrpc_core import register_method
from django.http import HttpRequest from django.http import HttpRequest
from django.urls import reverse
from hotpocket_backend.apps.core.rpc import wrap_soa_errors from hotpocket_backend.apps.core.rpc import wrap_soa_errors
from hotpocket_backend.apps.ui.dto.rpc import SavesCreateOut
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
from hotpocket_soa.dto.associations import AssociationOut
@register_method(method='saves.create') @register_method(method='saves.create')
@wrap_soa_errors @wrap_soa_errors
def create(request: HttpRequest, url: str) -> AssociationOut: def create(request: HttpRequest, url: str) -> SavesCreateOut:
association = CreateSaveWorkflow().run_rpc( association = CreateSaveWorkflow().run_rpc(
request=request, request=request,
account=request.user, account=request.user,
url=url, url=url,
) )
return association result = SavesCreateOut.model_validate({
'id': association.pk,
'target_uuid': association.target_uuid,
'url': request.build_absolute_uri(reverse(
'ui.associations.view', args=(association.pk,),
)),
})
return result

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -45,7 +45,6 @@
return null; return null;
} }
onLoad = (event) => { onLoad = (event) => {
console.log('HotPocketApp.onLoad()', event);
for (let pluginSpec of this.plugins) { for (let pluginSpec of this.plugins) {
if (pluginSpec[1].onLoad) { if (pluginSpec[1].onLoad) {
pluginSpec[1].onLoad(event); pluginSpec[1].onLoad(event);

View File

@@ -36,6 +36,12 @@
{% blocktranslate %}Log in with {{ HOTPOCKET_OIDC_DISPLAY_NAME }}{% endblocktranslate %} {% blocktranslate %}Log in with {{ HOTPOCKET_OIDC_DISPLAY_NAME }}{% endblocktranslate %}
</a> </a>
{% endif %} {% endif %}
{% if OPERATOR_EMAIL %}
<p class="my-0 mt-2 text-center">
<small>{% blocktranslate %}For support with your account, contact the <a href="mailto:{{ OPERATOR_EMAIL }}">instance operator</a>.{% endblocktranslate %}</small>
</p>
{% endif %}
</div> </div>
</div> </div>
{% include "ui/ui/partials/uname.html" %} {% include "ui/ui/partials/uname.html" %}

View File

@@ -16,7 +16,7 @@
{% endif %} {% endif %}
</div> </div>
<div class="card-footer d-flex align-items-center"> <div class="card-footer d-flex align-items-center">
<a href="{{ association.target.url }}" target="_blank" rel="noopener noreferrer"><small>{{ association.target.url|render_url_domain }}</small></a> <a href="{{ association.target.url }}" target="_blank" rel="noopener noreferrer"><small>{{ association.target.url|render_url_domain }} <i class="bi bi-box-arrow-up-right"></i></small></a>
<div class="ms-auto flex-shrink-0 d-flex align-items-center"> <div class="ms-auto flex-shrink-0 d-flex align-items-center">
{% if not association.archived_at %} {% if not association.archived_at %}
<div class="spinner-border spinner-border-sm ui-htmx-indicator" role="status"> <div class="spinner-border spinner-border-sm ui-htmx-indicator" role="status">

View File

@@ -2,10 +2,24 @@
{% load i18n static ui %} {% load i18n static ui %}
{% block title %}{{ association.title }}{% endblock %} {% block title %}{% if association.title %}{{ association.title }}{% else %}{{ association.target.url }}{% endif %}{% endblock %}
{% block button_bar_class %}d-none{% endblock %} {% block button_bar_class %}d-none{% endblock %}
{% block page_head %}
{{ block.super}}
<meta property="og:title" content="{% if association.title %}{{ association.title }}{% else %}{{ association.target.url }}{% endif %}">
{% if association.description %}
<meta property="og:description" content="{{ association.description|truncatechars:125 }}">
{% endif %}
<meta property="og:image" content="{{ og_card_url }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:url" content="{{ share_url }}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{ SITE_TITLE }}">
{% endblock %}
{% block page_body %} {% block page_body %}
<div id="ViewAssociationView" class="container pb-3"> <div id="ViewAssociationView" class="container pb-3">
<p class="display-3 mt-3 mb-0 text-center"> <p class="display-3 mt-3 mb-0 text-center">

View File

@@ -130,6 +130,56 @@
{% translate 'Log out' %} {% translate 'Log out' %}
</a> </a>
</li> </li>
<li class="nav-item px-3 d-flex justify-content-center">
{% spaceless %}
<a
class="btn btn-outline-info btn-sm"
href="https://apps.apple.com/pl/app/hotpocket-by-bthlabs/id6752321380"
rel="noopener noreferrer"
target="_blank"
title="{% translate 'Safari and Share extension for iOS and macOS' %}"
>
<i class="bi bi-apple"></i>
</a>
<a
class="btn btn-outline-info btn-sm ms-1"
href="https://chromewebstore.google.com/detail/save-to-hotpocket/mkmoejhhgnadmijpgkkioicjmikkkjbd"
rel="noopener noreferrer"
target="_blank"
title="{% translate 'Chrome extension' %}"
>
<i class="bi bi-browser-chrome"></i>
</a>
<a
class="btn btn-outline-info btn-sm ms-1"
href="https://addons.mozilla.org/en-GB/firefox/addon/save-to-hotpocket/"
rel="noopener noreferrer"
target="_blank"
title="{% translate 'Firefox extension' %}"
>
<i class="bi bi-browser-firefox"></i>
</a>
<a
class="btn btn-outline-info btn-sm ms-1"
data-bs-toggle="modal"
data-bs-target="#modalPWA"
href="#"
title="{% translate 'Android PWA' %}"
>
<i class="bi bi-android2"></i>
</a>
<div class="vr my-1 ms-1 opacity-75"></div>
<a
class="btn btn-outline-info btn-sm ms-1"
href="https://git.bthlabs.pl/tomekwojcik/hotpocket"
rel="noopener noreferrer"
target="_blank"
title="{% translate 'Source code repository' %}"
>
<i class="bi bi-git"></i>
</a>
{% endspaceless %}
</li>
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'ui.accounts.login' %}"> <a class="nav-link" href="{% url 'ui.accounts.login' %}">
@@ -142,6 +192,38 @@
{% include "ui/ui/partials/uname.html" %} {% include "ui/ui/partials/uname.html" %}
</div> </div>
</div> </div>
{% if not request.user.is_anonymous %}
<div class="modal modal-fade" id="modalPWA" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modalPWALabel">
{% translate 'HotPocket on Android and others' %}
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-1">
{% blocktranslate %}
HotPocket doesn't natively support Android and other systems. However, it's a Progressive Web Application. You can install it from your browser and it'll register itself as share target.
{% endblocktranslate %}
</p>
<p class="mb-0">
{% blocktranslate %}
This is currently supported on Android and Windows 11 (when installed using Edge).
{% endblocktranslate %}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">
{% translate 'Cool' %}
</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}

View File

@@ -3,11 +3,13 @@
{% if save.is_youtube_video %} {% if save.is_youtube_video %}
<div class="mb-0 d-flex justify-content-center"> <div class="mb-0 d-flex justify-content-center">
<iframe <iframe
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allow="accelerometer *; clipboard-write *; encrypted-media *; gyroscope *; picture-in-picture *; web-share *;"
allowfullscreen allowfullscreen
class="ui-youtube-iframe" class="ui-youtube-iframe"
frameborder="0" frameborder="0"
height="200" height="200"
referrerpolicy="strict-origin"
scrolling="no"
src="{{ save|render_youtube_embed_url }}" src="{{ save|render_youtube_embed_url }}"
title="YouTube video player" title="YouTube video player"
width="320" width="320"

View File

@@ -8,6 +8,7 @@ from django.contrib import messages
import django.db import django.db
from django.http import HttpRequest, HttpResponse, JsonResponse from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.templatetags.static import static
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, View from django.views.generic import FormView, View
@@ -198,12 +199,18 @@ def view(request: HttpRequest, pk: uuid.UUID) -> HttpResponse:
if is_share is True: if is_share is True:
show_controls = show_controls and False show_controls = show_controls and False
share_url = reverse( share_url = request.build_absolute_uri(
'ui.associations.view', reverse(
args=(association.pk,), 'ui.associations.view',
query=[ args=(association.pk,),
('share', 'true'), query=[
], ('share', 'true'),
],
),
)
og_card_url = request.build_absolute_uri(
static('ui/img/og-card.png'),
) )
return render( return render(
@@ -213,6 +220,12 @@ def view(request: HttpRequest, pk: uuid.UUID) -> HttpResponse:
'association': association, 'association': association,
'show_controls': show_controls, 'show_controls': show_controls,
'share_url': share_url, 'share_url': share_url,
'is_share': is_share,
'og_card_url': (
og_card_url
if is_share is True
else None
),
}, },
) )

View File

@@ -27,9 +27,6 @@ def page_not_found(request: HttpRequest,
exception: Exception, exception: Exception,
template_name: str = ERROR_404_TEMPLATE_NAME, template_name: str = ERROR_404_TEMPLATE_NAME,
) -> HttpResponseNotFound: ) -> HttpResponseNotFound:
if exception:
LOGGER.error('Exception: %s', exception, exc_info=exception)
return HttpResponseNotFound(render_to_string( return HttpResponseNotFound(render_to_string(
'ui/errors/page_not_found.html', 'ui/errors/page_not_found.html',
context={}, context={},

View File

@@ -72,6 +72,7 @@ TEMPLATES = [
'hotpocket_backend.apps.ui.context_processors.debug', 'hotpocket_backend.apps.ui.context_processors.debug',
'hotpocket_backend.apps.ui.context_processors.version', 'hotpocket_backend.apps.ui.context_processors.version',
'hotpocket_backend.apps.ui.context_processors.appearance_settings', 'hotpocket_backend.apps.ui.context_processors.appearance_settings',
'hotpocket_backend.apps.ui.context_processors.operator_email',
], ],
}, },
}, },
@@ -295,3 +296,5 @@ SAVES_ASSOCIATION_ADAPTER = os.environ.get(
UPLOADS_PATH = None UPLOADS_PATH = None
SITE_SHORT_TITLE = 'HotPocket' SITE_SHORT_TITLE = 'HotPocket'
OPERATOR_EMAIL = os.environ.get('HOTPOCKET_BACKEND_OPERATOR_EMAIL', None)

View File

@@ -13,7 +13,7 @@ cat <<EOF
|_| |_|
production production
HotPocket v25.11.18 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/) HotPocket v26.1.14 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/)
Copyright 2025-present by BTHLabs. All rights reserved. (https://bthlabs.pl/) Copyright 2025-present by BTHLabs. All rights reserved. (https://bthlabs.pl/)
Licensed under Apache-2.0 Licensed under Apache-2.0
EOF EOF

View File

@@ -1,6 +1,6 @@
{ {
"name": "hotpocket-backend", "name": "hotpocket-backend",
"version": "25.11.18", "version": "26.1.14",
"description": "HotPocket Backend", "description": "HotPocket Backend",
"main": "hotpocket_backend/apps/frontend/src/index.js", "main": "hotpocket_backend/apps/frontend/src/index.js",
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket", "repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "hotpocket-backend" name = "hotpocket-backend"
version = "25.11.18" version = "26.1.14"
description = "HotPocket Backend" description = "HotPocket Backend"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"] authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0" license = "Apache-2.0"

View File

@@ -103,7 +103,7 @@ def ci(ctx: Context):
@task @task
def setup(ctx: Context): def setup(ctx: Context):
ctx.run('python manage.py migrate') ctx.run('python manage.py migrate')
ctx.run('python manage.py create_initial_account hotpocket hotpocketm4st3r') ctx.run('python manage.py create_initial_account -u hotpocket -p hotpocketm4st3r')
if WORKSPACE_MODE == WorkspaceMode.METAL: if WORKSPACE_MODE == WorkspaceMode.METAL:
ctx.run('mkdir -p run/uploads') ctx.run('mkdir -p run/uploads')

View File

@@ -28,13 +28,15 @@ def test_authenticated_ok(authenticated_client: Client,
assert result.context['association'].target.pk == association_out.target.pk assert result.context['association'].target.pk == association_out.target.pk
assert result.context['show_controls'] is True assert result.context['show_controls'] is True
assert 'share_url' in result.context assert 'share_url' in result.context
assert result.context['is_share'] is False
assert result.context['og_card_url'] is None
expected_share_url = reverse( expected_share_url = reverse(
'ui.associations.view', 'ui.associations.view',
args=(association_out.pk,), args=(association_out.pk,),
query=[('share', 'true')], query=[('share', 'true')],
) )
assert result.context['share_url'] == expected_share_url assert result.context['share_url'].endswith(expected_share_url)
@pytest.mark.django_db @pytest.mark.django_db
@@ -126,6 +128,16 @@ def test_authenticated_share_ok(authenticated_client: Client,
assert hasattr(result.context['association'], 'target') is True assert hasattr(result.context['association'], 'target') is True
assert result.context['association'].target.pk == other_account_association_out.target.pk assert result.context['association'].target.pk == other_account_association_out.target.pk
assert result.context['show_controls'] is False assert result.context['show_controls'] is False
assert 'share_url' in result.context
assert result.context['is_share'] is True
assert result.context['og_card_url'] is not None
expected_share_url = reverse(
'ui.associations.view',
args=(other_account_association_out.pk,),
query=[('share', 'true')],
)
assert result.context['share_url'].endswith(expected_share_url)
@pytest.mark.django_db @pytest.mark.django_db

View File

@@ -54,8 +54,12 @@ def test_ok(authenticated_client: Client,
call_result = result.json() call_result = result.json()
assert 'error' not in call_result assert 'error' not in call_result
save_pk = uuid.UUID(call_result['result']['target_uuid'])
association_pk = uuid.UUID(call_result['result']['id']) association_pk = uuid.UUID(call_result['result']['id'])
save_pk = uuid.UUID(call_result['result']['target_uuid'])
assert call_result['result']['url'].endswith(reverse(
'ui.associations.view', args=(association_pk,),
))
AssociationsTestingService().assert_created( AssociationsTestingService().assert_created(
pk=association_pk, pk=association_pk,

View File

@@ -1,3 +1,36 @@
# HotPocket by BTHLabs # HotPocket by BTHLabs
This repository contains the _HotPocket Extension_ project. This repository contains the _HotPocket Extension_ project.
## Development environment setup
### Requirements
* macOS (18 or newer) or modern Linux (including WSL2).
* nodejs v22.14 or later.
* yarnpkg v1.22.22 or newer.
### Setup
1. `yarn install`
## Building browser-specific extensions
To build a browser-specific extension, use the following command:
```
$ yarn build:<target>
```
Where `<target>` is the name of the browser. The currently supported targets
are: `safari`, `firefox`, `chrome`. The build result will be placed in
`dist/<target>-production` for Firefox and Chrome extensions. Safari
extension will be built in the `apple` service tree.
## Author
_HotPocket_ is developed by [BTHLabs](https://www.bthlabs.pl/).
## License
_HotPocket_ is licensed under the Apache 2.0 License.

View File

@@ -30,5 +30,9 @@
"content_popup_content_error_message": { "content_popup_content_error_message": {
"message": "HotPocket couldn't complete this operation.", "message": "HotPocket couldn't complete this operation.",
"description": "Title of the error content popup." "description": "Title of the error content popup."
},
"content_popup_content_view_button_message": {
"message": "View in HotPocket",
"description": "Title of the view link button"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "hotpocket-extension", "name": "hotpocket-extension",
"version": "25.11.18", "version": "25.11.26",
"description": "HotPocket Extension", "description": "HotPocket Extension",
"main": "src/index.js", "main": "src/index.js",
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket", "repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "hotpocket-extension" name = "hotpocket-extension"
version = "25.11.18" version = "25.11.26"
description = "HotPocket Extension" description = "HotPocket Extension"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"] authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0" license = "Apache-2.0"

View File

@@ -117,10 +117,10 @@ const doSave = async (accessToken, tab) => {
); );
if (error !== null) { if (error !== null) {
return false; return null;
} }
return true; return result;
}; };
const doCreateAndStoreAccessToken = async (authKey) => { const doCreateAndStoreAccessToken = async (authKey) => {

View File

@@ -19,18 +19,32 @@ class Popup {
this.timeout = null; this.timeout = null;
} }
}; };
setContent = (content) => { replaceInnerHTML = (element, content) => {
for (let child of element.childNodes) {
element.removeChild(child);
}
element.insertAdjacentHTML('beforeend', content);
};
setContent = (content, saveUrl) => {
const shadow = this.container.shadowRoot; const shadow = this.container.shadowRoot;
const body = shadow.querySelector('.hotpocket-extension-popup-body'); const body = shadow.querySelector('.hotpocket-extension-popup-body');
body.innerHTML = content; this.replaceInnerHTML(body, content);
const i18nElements = shadow.querySelectorAll('[data-message]'); const i18nElements = shadow.querySelectorAll('[data-message]');
for (let i18nElement of i18nElements) { for (let i18nElement of i18nElements) {
i18nElement.innerHTML = HotPocketExtension.api.i18n.getMessage( i18nElement.textContent = HotPocketExtension.api.i18n.getMessage(
i18nElement.dataset.message, i18nElement.dataset.message,
); );
} }
if (saveUrl) {
const viewLink = shadow.querySelector('.hotpocket-extension-popup-view');
if (viewLink) {
viewLink.href = saveUrl;
}
}
}; };
close = () => { close = () => {
this.clearCloseTimeout(); this.clearCloseTimeout();
@@ -40,7 +54,7 @@ class Popup {
this.container = null; this.container = null;
} }
}; };
show = (content) => { show = (content, saveUrl) => {
this.close(); this.close();
this.container = document.createElement('div'); this.container = document.createElement('div');
@@ -48,9 +62,9 @@ class Popup {
this.container.hotPocketExtensionPopup = this; this.container.hotPocketExtensionPopup = this;
const shadow = this.container.attachShadow({mode: 'open'}); const shadow = this.container.attachShadow({mode: 'open'});
shadow.innerHTML = POPUP; shadow.setHTMLUnsafe(POPUP);
this.setContent(content); this.setContent(content, saveUrl);
const closeElements = shadow.querySelectorAll('.hotpocket-extension-popup-close'); const closeElements = shadow.querySelectorAll('.hotpocket-extension-popup-close');
for (const closeElement of closeElements) { for (const closeElement of closeElements) {
@@ -59,8 +73,8 @@ class Popup {
document.body.appendChild(this.container); document.body.appendChild(this.container);
}; };
update = (content) => { update = (content, saveUrl) => {
this.setContent(content); this.setContent(content, saveUrl);
}; };
onCloseClick = (event) => { onCloseClick = (event) => {
this.close(); this.close();
@@ -79,16 +93,16 @@ const doHandleBrowserActionClickedMessage = (message) => {
}; };
const doHandleSaveMessage = (message) => { const doHandleSaveMessage = (message) => {
let content = POPUP_CONTENT_ERROR; let content = POPUP_CONTENT_SUCCESS;
if (message.result === true) { if (message.result === null) {
content = POPUP_CONTENT_SUCCESS; content = POPUP_CONTENT_ERROR;
} }
if (currentPopup === null) { if (currentPopup === null) {
currentPopup = new Popup(); currentPopup = new Popup();
currentPopup.show(content); currentPopup.show(content);
} else { } else {
currentPopup.update(content); currentPopup.update(content, (message.result || {}).url);
} }
}; };

File diff suppressed because one or more lines are too long

View File

@@ -94,6 +94,26 @@
opacity: 0.83; opacity: 0.83;
transform: rotate(60deg); transform: rotate(60deg);
} }
.hotpocket-extension-popup-view {
background-color: #1CBAED;
border: 1px solid #1CBAED;
border-radius: 0.25rem;
color: white;
display: inline-block;
font-size: 0.875rem;
line-height: 1.25;
margin-top: 0.25rem !important;
padding: 0.25rem 0.5rem;
text-decoration: none;
}
.hotpocket-extension-popup-view:hover {
background-color: #189ec9;
border-color: #1695be;
}
.hotpocket-extension-popup-view:active {
background-color: #1695be;
border-color: #158cb2;
}
@keyframes hotpocket-extension-popup-loader-animation { @keyframes hotpocket-extension-popup-loader-animation {
100% {transform: rotate(1turn)} 100% {transform: rotate(1turn)}
} }
@@ -104,6 +124,6 @@
<strong>HotPocket by BTHLabs</strong> <strong>HotPocket by BTHLabs</strong>
<a class="hotpocket-extension-popup-close">&times;</a> <a class="hotpocket-extension-popup-close">&times;</a>
</div> </div>
<div class="hotpocket-extension-popup-body"> <div class="hotpocket-extension-popup-body hotpocket-extension-popup-message-success">
</div> </div>
</div> </div>

View File

@@ -2,4 +2,13 @@
<strong data-message="content_popup_content_success_title"></strong> <strong data-message="content_popup_content_success_title"></strong>
<br> <br>
<span data-message="content_popup_content_success_message"></span> <span data-message="content_popup_content_success_message"></span>
<br>
<a
class="hotpocket-extension-popup-view"
data-message="content_popup_content_view_button_message"
href="#"
rel="noopener noreferrer"
target="_blank"
>
</a>
</p> </p>

View File

@@ -3,7 +3,7 @@
"default_locale": "en", "default_locale": "en",
"name": "__MSG_extension_name__", "name": "__MSG_extension_name__",
"description": "__MSG_extension_description__", "description": "__MSG_extension_description__",
"version": "25.11.18", "version": "25.11.26",
"icons": { "icons": {
"16": "images/icon-16.png", "16": "images/icon-16.png",
"32": "images/icon-32.png", "32": "images/icon-32.png",

View File

@@ -14,7 +14,7 @@
}, },
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "@Extension.HotPocket.BTHLabs", "id": "@Production.Extension.HotPocket.BTHLabs",
"strict_min_version": "142.0", "strict_min_version": "142.0",
"data_collection_permissions": { "data_collection_permissions": {
"required": [ "required": [
@@ -24,4 +24,4 @@
} }
} }
} }
} }

View File

@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import json
import os import os
import typing import typing
@@ -14,7 +13,10 @@ import werkzeug
import werkzeug.routing import werkzeug.routing
from hotpocket_workspace_tools import get_workspace_mode from hotpocket_workspace_tools import get_workspace_mode
from hotpocket_workspace_tools.tasks import * # noqa: F401,F403 from hotpocket_workspace_tools.tasks import ( # noqa: F401
bump_version,
get_version,
)
WORKSPACE_MODE = get_workspace_mode() WORKSPACE_MODE = get_workspace_mode()
@@ -198,26 +200,54 @@ def build_safari(ctx: Context):
@task(pre=[clean]) @task(pre=[clean])
def build_chrome(ctx: Context): def build_chrome(ctx: Context):
ctx.run('yarn build:chrome') ctx.run('yarn build:chrome')
ctx.run(' '.join([
r'/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome', with ctx.cd('dist/chrome-production'):
'--pack-extension=dist/chrome-production/', current_version = get_version(ctx)
'--pack-extension-key=secrets/chrome.pem',
])) ctx.run(' '.join([
'zip',
'-r',
f'../chrome-production-{current_version}.zip',
'.',
]))
@task(pre=[clean]) @task(pre=[clean])
def build_firefox(ctx: Context): def build_firefox(ctx: Context):
ctx.run('yarn build:firefox') ctx.run('yarn build:firefox')
firefox_secrets = None
with open('secrets/firefox.json', 'r', encoding='utf-8') as firefox_secrets_f:
firefox_secrets = json.load(firefox_secrets_f)
with ctx.cd('dist/firefox-production'): with ctx.cd('dist/firefox-production'):
ctx.run(' '.join([ ctx.run(' '.join([
'web-ext', 'web-ext',
'sign', 'lint',
'--channel=unlisted', ]))
f'--api-key={firefox_secrets["api_key"]}',
f'--api-secret={firefox_secrets["api_secret"]}', current_version = get_version(ctx)
ctx.run(' '.join([
'zip',
'-r',
f'../firefox-production-{current_version}.zip',
'.',
]))
@task
def build_firefox_source(ctx: Context):
# AMO requires source bundle to be uploaded alongside the built version.
ctx.run('rm -rf dist/firefox-source')
ctx.run('mkdir -p dist/firefox-source dist/firefox-source/assets dist/firefox-source/src')
ctx.run('rsync -arv assets/ dist/firefox-source/assets/')
ctx.run('rsync -arv src/ dist/firefox-source/src/')
ctx.run('rsync -arv eslint.config.js package.json README.md rollup.config.js yarn.lock dist/firefox-source/')
with ctx.cd('dist/firefox-source'):
current_version = get_version(ctx)
ctx.run(' '.join([
'zip',
'-r',
f'../firefox-source-{current_version}.zip',
'.',
])) ]))

View File

@@ -1 +1 @@
from .utils import bump_version # noqa: F401 from .utils import bump_version, get_version # noqa: F401

View File

@@ -4,12 +4,16 @@ from __future__ import annotations
from invoke import Context, task from invoke import Context, task
def get_version(ctx: Context) -> str:
result = ctx.run('poetry version -s --no-ansi', hide='out')
assert result is not None, 'Hm?'
return result.stdout.strip()
@task @task
def bump_version(ctx: Context, next_version: str, build: str | None = None): def bump_version(ctx: Context, next_version: str, build: str | None = None):
current_version_result = ctx.run('poetry version -s --no-ansi', hide='out') current_version = get_version(ctx)
assert current_version_result is not None, 'Hm?'
current_version = current_version_result.stdout.strip()
print(f'Bumping version: `{current_version}` -> `{next_version}`') print(f'Bumping version: `{current_version}` -> `{next_version}`')