41 Commits

Author SHA1 Message Date
0ac2ca73ec Release v25.10.13
All checks were successful
CI / Checks (push) Successful in 3m58s
2025-10-13 21:46:18 +02:00
7b67a2f758 BTHLABS-62: Display progress in extension popup
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-10-13 18:48:00 +00:00
8b86145519 BTHLABS-61: Service layer refactoring
A journey to fix `ValidationError` in Pocket imports turned service
layer refactoring :D
2025-10-12 20:54:00 +02:00
ac7a8dd90e BTHLABS-0000: README.md fixes 2025-10-07 08:46:50 +02:00
6903b7f768 BTHLABS-0000: Nuked dotcom service
Moved to a separate repo
2025-10-07 08:45:10 +02:00
2e8b8d7330 BTHLABS-60: Appearance settings
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-10-07 04:42:58 +00:00
b4d5375954 BTHLABS-0000: Docker and CI tweaks
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-10-07 04:37:01 +00:00
3f3f90103c Release v25.10.4
All checks were successful
CI / Checks (push) Successful in 24m41s
2025-10-04 08:07:26 +02:00
8582c12ec7 BTHLABS-58: UI updates for Apple Apps 2025-10-04 08:07:00 +02:00
98b3798264 Release v25.10.3 2025-10-04 08:06:36 +02:00
6332a9cef9 BTHLABS-58: Tweaks and fixes
* Use explicit values to populate access token's platform in apps.
* Fix View Association layout.
* Web Extension popup layout rework.
2025-10-04 08:06:18 +02:00
efcce32b50 Release v25.10.2 2025-10-04 08:04:17 +02:00
0311a28571 BTHLABS-0000: Stop App Store Connect from nagging about encryption 2025-10-04 08:04:00 +02:00
cb001f7e91 BTHLABS-58: Fixing URL paths resolution to avoid double slashes 2025-10-04 08:03:42 +02:00
9ab2b304b8 Release v25.10.1 2025-10-04 08:03:07 +02:00
1fd4dd735d BTHLABS-58: Cleaning up and preparing Apple Apps for release 2025-10-04 08:02:49 +02:00
99e9226338 BTHLABS-58: Share Extension in Apple Apps 2025-10-04 08:02:13 +02:00
0c12f52569 Release v25.9.18
All checks were successful
CI / Checks (push) Successful in 18m22s
2025-09-18 20:43:05 +02:00
a6f01ba71e BTHLABS-0000: Fix bumping of the workspace 2025-09-18 20:42:49 +02:00
77526b1fae BTHLABS-0000: Allow bumping a single service from top-level tasks. 2025-09-18 20:41:04 +02:00
7c97445155 BTHLABS-0000: Fix a bug that prevented RPC-created saves from processing 2025-09-18 20:41:04 +02:00
a0f1a4ce80 Release v25.9.17
All checks were successful
CI / Checks (push) Successful in 16m16s
2025-09-17 20:38:59 +02:00
80fbbcddf3 BTHLABS-0000: bump-version task
Enough with manual version bumps :D
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-17 20:27:20 +02:00
0ab87e25a4 BTHLABS-0000: Add scope to PWA manifest so share sheet target. 2025-09-17 20:27:20 +02:00
495255206e BTHLABS-57: Pre-auth page in the extension 2025-09-17 20:27:20 +02:00
46254730bd BTHLABS-52: Firefox Desktop Extension 2025-09-17 20:27:08 +02:00
d1e60babf4 BTHLABS-56: _Copy share link_ button in view association page
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-15 06:28:38 +00:00
ab84f685c0 BTHLABS-51: Chrome Web Extension
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-14 06:34:43 +00:00
1a8c4bfebc BTHLABS-0000: eslint.config.js fixes and code cleanup 2025-09-13 09:05:29 +02:00
b15b48f702 BTHLABS-55: Inline create save form
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-13 06:56:44 +00:00
29c732faa0 Release v25.9.12
All checks were successful
CI / Checks (push) Successful in 15m7s
2025-09-11 20:50:18 +02:00
dcebccf947 BTHLABS-50: Safari Web Extension: Reloaded
Turns out, getting this thing out into the wild isn't as simple as I thought :D
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-11 15:57:11 +00:00
67138c7035 BTHLABS-0000: Use absolute URLs in ui.meta.manifest_json 2025-09-09 15:21:45 +02:00
6f848be1ee Release v25.9.8
All checks were successful
CI / Checks (push) Successful in 14m34s
2025-09-08 21:06:53 +02:00
b6d02dbe78 BTHLABS-50: Safari Web extension
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-09-08 18:11:36 +00:00
ffecf780ee BTHLABS-0000: Enable actions on development branch 2025-08-30 08:42:01 +02:00
fad9d2d26f BTHLABS-0000: Filter ci workflow's triggers. 2025-08-30 08:01:31 +02:00
f68e5b4573 Release v1.0.2
All checks were successful
CI / Checks (push) Successful in 16m8s
2025-08-29 14:15:16 +02:00
db39c38594 BTHLABS-54: Unprocessed save shows Untitled in the card title 2025-08-29 08:23:09 +02:00
38d768a584 BTHLABS-53: Processing task fails for newly created saves 2025-08-29 08:20:53 +02:00
fb39818be3 public -> development 2025-08-20 20:01:12 +00:00
352 changed files with 16001 additions and 560 deletions

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -e
set +x
set -o pipefail
cat >"./docker-compose-ci-${COMPOSE_PROJECT}.yaml" <<EOF
services:
postgres:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-${COMPOSE_PROJECT}"
keycloak:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-${COMPOSE_PROJECT}"
rabbitmq:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-${COMPOSE_PROJECT}"
apple-ci:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/apple:ci-${COMPOSE_PROJECT}"
backend-ci:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-${COMPOSE_PROJECT}"
extension-ci:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-${COMPOSE_PROJECT}"
packages-ci:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-${COMPOSE_PROJECT}"
EOF

View File

@@ -2,7 +2,13 @@ name: "CI"
on: on:
push: push:
branches:
- "development"
- "public"
pull_request: pull_request:
branches:
- "development"
- "public"
jobs: jobs:
run-checks: run-checks:
@@ -11,8 +17,35 @@ jobs:
steps: steps:
- name: "Checkout the code" - name: "Checkout the code"
uses: "actions/checkout@v2" uses: "actions/checkout@v2"
- name: "Get run info"
id: "get-run-info"
run: |
set -x
echo "COMPOSE_PROJECT=${{ vars.COMPOSE_PROJECT_BASE }}-${GITHUB_RUN_NUMBER}" >> $GITHUB_OUTPUT
- name: "Get build options"
id: "get-build-options"
run: |
set -x
SHORT_SHA="${GITHUB_SHA::8}"
BUILD_ARCH="amd64"
BUILD_PLATFORM="linux/amd64"
if [ "${RUNNER_ARCH}" = "ARM64" ];then
BUILD_ARCH="arm64"
BUILD_PLATFORM="linux/arm64"
fi
echo "SHORT_SHA=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "BUILD_ARCH=$BUILD_ARCH" >> $GITHUB_OUTPUT
echo "BUILD_PLATFORM=$BUILD_PLATFORM" >> $GITHUB_OUTPUT
- name: "Set up Docker Buildx" - name: "Set up Docker Buildx"
id: "setup-docker-buildx"
uses: "docker/setup-buildx-action@v3" uses: "docker/setup-buildx-action@v3"
with:
driver: "remote"
endpoint: "tcp://builder-01.bthlab:2375"
platforms: "linux/amd64"
append: |
- endpoint: "tcp://builder-mac-01.bthlab:2375"
platforms: "linux/arm64"
- name: "Build `postgres` image" - name: "Build `postgres` image"
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -20,7 +53,10 @@ jobs:
context: "services/" context: "services/"
push: false push: false
load: true load: true
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-local" tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/postgres:15.13-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
- name: "Build `keycloak` image" - name: "Build `keycloak` image"
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -28,7 +64,10 @@ jobs:
context: "services/" context: "services/"
push: false push: false
load: true load: true
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-local" tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/keycloak:22.0.3-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
- name: "Build `rabbitmq` image" - name: "Build `rabbitmq` image"
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -36,7 +75,10 @@ jobs:
context: "services/" context: "services/"
push: false push: false
load: true load: true
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-local" tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/rabbitmq:3.10.8-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
- name: "Build `backend-ci` image" - name: "Build `backend-ci` image"
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -45,7 +87,10 @@ jobs:
target: "ci" target: "ci"
push: false push: false
load: true load: true
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-local" tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:ci-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
- name: "Build `packages-ci` image" - name: "Build `packages-ci` image"
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -54,17 +99,103 @@ jobs:
target: "ci" target: "ci"
push: false push: false
load: true load: true
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-local" tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/packages:ci-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
- name: "Build `extension-ci` image"
uses: docker/build-push-action@v6
with:
file: "services/extension/Dockerfile"
context: "services/"
target: "ci"
push: false
load: true
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
- name: "Build `apple-ci` image"
uses: docker/build-push-action@v6
with:
file: "services/apple/Dockerfile"
context: "services/"
target: "ci"
push: false
load: true
tags: "docker-hosted.nexus.bthlabs.pl/hotpocket/apple:ci-${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
platforms: "${{ steps.get-build-options.outputs.BUILD_PLATFORM }}"
cache-from: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket"
cache-to: "type=registry,ref=nexus.bthlab.bthlabs.net:8001/hotpocket,mode=max"
- name: "Prepare the build"
id: "prepare"
env:
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
run: |
set -x
./.gitea/tools/render-docker-compose-ci.sh
- name: "Run `backend` checks" - name: "Run `backend` checks"
if: "steps.prepare.conclusion == 'success'"
env:
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
run: | run: |
set -x set -x
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm backend-ci inv ci docker compose \
-p "${COMPOSE_PROJECT}" \
-f "docker-compose.yaml" \
-f "docker-compose-ci.yaml" \
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
run --rm \
backend-ci inv ci
- name: "Run `packages` checks" - name: "Run `packages` checks"
if: "steps.prepare.conclusion == 'success'"
env:
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
run: | run: |
set -x set -x
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml run --rm packages-ci inv ci docker compose \
-p "${COMPOSE_PROJECT}" \
-f "docker-compose.yaml" \
-f "docker-compose-ci.yaml" \
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
run --rm \
packages-ci inv ci
- name: "Run `extension` checks"
if: "steps.prepare.conclusion == 'success'"
env:
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
run: |
set -x
docker compose \
-p "${COMPOSE_PROJECT}" \
-f "docker-compose.yaml" \
-f "docker-compose-ci.yaml" \
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
run --rm \
extension-ci inv ci
- name: "Run `apple` checks"
if: "steps.prepare.conclusion == 'success'"
env:
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
run: |
set -x
docker compose \
-p "${COMPOSE_PROJECT}" \
-f "docker-compose.yaml" \
-f "docker-compose-ci.yaml" \
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
run --rm \
apple-ci inv ci
- name: "Clean up" - name: "Clean up"
if: always() if: always()
env:
COMPOSE_PROJECT: "${{ steps.get-run-info.outputs.COMPOSE_PROJECT }}"
run: | run: |
set -x set -x
docker compose -f docker-compose.yaml -f docker-compose-ci.yaml down --volumes docker compose \
-p "${COMPOSE_PROJECT}" \
-f "docker-compose.yaml" \
-f "docker-compose-ci.yaml" \
-f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" \
down --volumes --rmi all || true
rm -f "docker-compose-ci-${COMPOSE_PROJECT}.yaml" || true

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.envrc* .envrc*
.ipythonhome/ .ipythonhome/
/docker-compose-ci-*.yaml

View File

@@ -86,3 +86,5 @@ Licensed under terms of the MIT License
Pepper Hot Solid icon Pepper Hot Solid icon
Copyright (c) Icons8 Copyright (c) Icons8
Licensed under terms of the MIT License Licensed under terms of the MIT License
Spinner Loader CSS from https://css-loaders.com/

View File

@@ -66,7 +66,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 \
docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v1.0.1-01 docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.10.13-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
@@ -76,8 +76,7 @@ credentials. The Web app will be reachable at `http://127.0.0.1:8000/`.
The admin will be reachable at `http://127.0.0.1:8000/admin/`. The admin will be reachable at `http://127.0.0.1:8000/admin/`.
The `DJANGO_SETTINGS_MODULE` environment variable defaults to The `DJANGO_SETTINGS_MODULE` environment variable defaults to
`hotpocket_backend.settings.deployment.webapp`. This should be set to `hotpocket_backend.settings.deployment.aio`.
`hotpocket_backend.settings.deployment.admin` in the Admin container.
**NOTE:** The command above specifies wildly insecure `SECRET_KEY` which is **NOTE:** The command above specifies wildly insecure `SECRET_KEY` which is
used among other things to secure the session cookie. Please *please* used among other things to secure the session cookie. Please *please*
@@ -94,7 +93,8 @@ backend etc. The final deployment will require services for at least the Web
app, the Celery worker and Celery Beat. Admin is optional. app, the Celery worker and Celery Beat. Admin is optional.
The `DJANGO_SETTINGS_MODULE` environment variable defaults to The `DJANGO_SETTINGS_MODULE` environment variable defaults to
`hotpocket_backend.settings.deployment.aio`. `hotpocket_backend.settings.deployment.webapp`. This should be set to
`hotpocket_backend.settings.deployment.admin` in the Admin container.
The `deployment/fullstack/docker-compose.yaml` file can be used as a The `deployment/fullstack/docker-compose.yaml` file can be used as a
starting point for full-stack deployments. starting point for full-stack deployments.

View File

@@ -1,6 +1,6 @@
services: services:
backend: backend:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v1.0.1-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.10.13-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: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v1.0.1-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.13-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: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v1.0.1-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.13-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: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v1.0.1-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.13-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: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v1.0.1-01" image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.10.13-01"
command: command:
- "/srv/venv/bin/celery" - "/srv/venv/bin/celery"
- "-A" - "-A"

View File

@@ -2,8 +2,10 @@
"group": { "group": {
"default": { "default": {
"targets": [ "targets": [
"apple-management",
"backend-management", "backend-management",
"caddy", "caddy",
"extension-management",
"keycloak", "keycloak",
"packages-management", "packages-management",
"postgres", "postgres",
@@ -12,6 +14,28 @@
} }
}, },
"target": { "target": {
"apple-management": {
"context": "services/",
"dockerfile": "apple/Dockerfile",
"tags": [
"docker-hosted.nexus.bthlabs.pl/hotpocket/apple:local"
],
"target": "development",
"output": [
"type=docker,load=true,push=false"
]
},
"apple-ci": {
"context": "services/",
"dockerfile": "apple/Dockerfile",
"tags": [
"docker-hosted.nexus.bthlabs.pl/hotpocket/apple:ci-local"
],
"target": "ci",
"output": [
"type=docker,load=true,push=false"
]
},
"backend-management": { "backend-management": {
"context": "services/", "context": "services/",
"dockerfile": "backend/Dockerfile", "dockerfile": "backend/Dockerfile",
@@ -67,6 +91,28 @@
"type=docker,load=true,push=false" "type=docker,load=true,push=false"
] ]
}, },
"extension-management": {
"context": "services/",
"dockerfile": "extension/Dockerfile",
"tags": [
"docker-hosted.nexus.bthlabs.pl/hotpocket/extension:local"
],
"target": "development",
"output": [
"type=docker,load=true,push=false"
]
},
"extension-ci": {
"context": "services/",
"dockerfile": "extension/Dockerfile",
"tags": [
"docker-hosted.nexus.bthlabs.pl/hotpocket/extension:ci-local"
],
"target": "ci",
"output": [
"type=docker,load=true,push=false"
]
},
"caddy": { "caddy": {
"context": "services/", "context": "services/",
"dockerfile": "caddy/Dockerfile", "dockerfile": "caddy/Dockerfile",

View File

@@ -1,15 +1,17 @@
services: services:
postgres: postgres:
ports: [] ports: !override []
keycloak: keycloak:
command: "echo 'NOOP'" command: "echo 'NOOP'"
ports: [] ports: !override []
restart: "no" restart: "no"
rabbitmq: rabbitmq:
ports: [] ports: !override []
include: include:
- path: "./services/backend/docker-compose-ci.yaml" - path: "./services/backend/docker-compose-ci.yaml"
- path: "./services/packages/docker-compose-ci.yaml" - path: "./services/packages/docker-compose-ci.yaml"
- path: "./services/extension/docker-compose-ci.yaml"
- path: "./services/apple/docker-compose-ci.yaml"

View File

@@ -5,6 +5,8 @@ include:
- path: "./docker-compose-cloud.yaml" - path: "./docker-compose-cloud.yaml"
- path: "./services/backend/docker-compose.yaml" - path: "./services/backend/docker-compose.yaml"
- path: "./services/packages/docker-compose.yaml" - path: "./services/packages/docker-compose.yaml"
- path: "./services/extension/docker-compose.yaml"
- path: "./services/apple/docker-compose.yaml"
volumes: {} volumes: {}

View File

@@ -1,6 +0,0 @@
{
"run": {
"echo": true,
"pty": true
}
}

8
invoke.yaml Normal file
View File

@@ -0,0 +1,8 @@
run:
echo: true
pty: true
files_to_version:
- "deployment/aio/docker-compose.yaml"
- "deployment/fullstack/docker-compose.yaml"
- "pyproject.toml"
- "README.md"

3
poetry.lock generated
View File

@@ -9,6 +9,9 @@ python-versions = "^3.12"
files = [] files = []
develop = true develop = true
[package.dependencies]
invoke = "2.2.0"
[package.source] [package.source]
type = "directory" type = "directory"
url = "services/packages/workspace_tools" url = "services/packages/workspace_tools"

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "hotpocket-workspace" name = "hotpocket-workspace"
version = "1.0.0" version = "25.10.13"
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

@@ -1,4 +1,8 @@
.mypy_cache/
.pytest_cache/
_tmp/ _tmp/
apple/build/
apple/DerivedData/
backend/node_modules/ backend/node_modules/
backend/ops/metal/ backend/ops/metal/
backend/hotpocket_backend/playground.py backend/hotpocket_backend/playground.py
@@ -7,4 +11,6 @@ backend/hotpocket_backend/settings/docker/
backend/hotpocket_backend/secrets/metal/ backend/hotpocket_backend/secrets/metal/
backend/hotpocket_backend/settings/metal/ backend/hotpocket_backend/settings/metal/
backend/hotpocket_backend/static/ backend/hotpocket_backend/static/
extension/node_modules/
extension/dist/
.envrc* .envrc*

94
services/apple/.gitignore vendored Normal file
View File

@@ -0,0 +1,94 @@
# Xcode
## Build generated
build/
DerivedData/
## Various settings
ExportOptions.plist
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
## We don't want any memgrap's to leak ;)
**.memgraph
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# Accio dependency management
Dependencies/
.accio/
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
fastlane/*.env
# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
# Extension stuff
Shared (Extension)/Resources/_locales/
Shared (Extension)/Resources/images/
Shared (Extension)/Resources/background-bundle.js
Shared (Extension)/Resources/content-bundle.js
Shared (Extension)/Resources/manifest.json
Shared (Extension)/Resources/preauth.html
Shared (Extension)/Resources/preauth.js

19
services/apple/Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
ARG APP_USER_UID=1000
ARG APP_USER_GID=1000
ARG IMAGE_ID=development.00000000
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-node-20250819-01 AS development
ARG APP_USER_UID
ARG APP_USER_GID
ARG IMAGE_ID
# COPY --chown=$APP_USER_UID:$APP_USER_GID apple/ops/bin/*.sh /srv/bin/
VOLUME ["/srv/node_modules", "/srv/venv"]
FROM development AS ci
COPY --chown=$APP_USER_UID:$APP_USER_GID apple/ /srv/app/
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/ /srv/packages/
COPY --chown=$APP_USER_UID:$APP_USER_GID tls/ /srv/tls/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

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

View File

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

View File

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

View File

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

View File

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

3
services/apple/README.md Normal file
View File

@@ -0,0 +1,3 @@
# HotPocket by BTHLabs
This repository contains the _HotPocket Apple Integrations_ project.

View File

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

View File

@@ -0,0 +1,93 @@
{
"images" : [
{
"filename" : "icon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "icon-1024 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"filename" : "icon-1024 2.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"filename" : "icon-16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon-32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon-32 1.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon-64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon-mac-1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

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

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "icon-large-128.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "icon-large-128@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "icon-large-128@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,12 @@
//
// SafariWebExtensionHandler.h
// Shared (Extension)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <Foundation/Foundation.h>
@interface SafariWebExtensionHandler : NSObject <NSExtensionRequestHandling>
@end

View File

@@ -0,0 +1,43 @@
//
// SafariWebExtensionHandler.m
// Shared (Extension)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "SafariWebExtensionHandler.h"
#import <SafariServices/SafariServices.h>
@implementation SafariWebExtensionHandler
- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context {
NSExtensionItem *request = context.inputItems.firstObject;
NSUUID *profile;
if (@available(iOS 17.0, macOS 14.0, *)) {
profile = request.userInfo[SFExtensionProfileKey];
} else {
profile = request.userInfo[@"profile"];
}
id message;
if (@available(iOS 15.0, macOS 11.0, *)) {
message = request.userInfo[SFExtensionMessageKey];
} else {
message = request.userInfo[@"message"];
}
NSLog(@"Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", message, profile.UUIDString ?: @"none");
NSExtensionItem *response = [[NSExtensionItem alloc] init];
if (@available(iOS 15.0, macOS 11.0, *)) {
response.userInfo = @{ SFExtensionMessageKey: @{ @"echo": message } };
} else {
response.userInfo = @{ @"message": @{ @"echo": message } };
}
[context completeRequestReturningItems:@[ response ] completionHandler:nil];
}
@end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
services:
apple-ci:
build:
context: ".."
dockerfile: "apple/Dockerfile"
target: "development"
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/apple:ci-local"
command: "echo 'NOOP'"
environment:
PYTHONBREAKPOINT: "ipdb.set_trace"
HOTPOCKET_PACKAGES_ENV: "${HOTPOCKET_EXTENSION_ENV:-docker}"
# REQUESTS_CA_BUNDLE: "/srv/tls/requests_ca_bundle.pem"
RUN_POETRY_INSTALL: "true"
RUN_YARN_INSTALL: "false"
SETUP_BACKEND: "true"
SETUP_FRONTEND: "false"
volumes:
- "apple_venv:/srv/venv"
- "apple_node_modules:/srv/node_modules"
- "../tls:/srv/tls"
restart: "no"
stdin_open: true
tty: true

View File

@@ -0,0 +1,29 @@
services:
apple-management:
build:
context: ".."
dockerfile: "apple/Dockerfile"
target: "development"
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/apple:local"
command: "echo 'NOOP'"
environment: &apple-env
PYTHONBREAKPOINT: "ipdb.set_trace"
HOTPOCKET_EXTENSION_ENV: "${HOTPOCKET_EXTENSION_ENV:-docker}"
REQUESTS_CA_BUNDLE: "/srv/tls/requests_ca_bundle.pem"
RUN_POETRY_INSTALL: "true"
RUN_YARN_INSTALL: "false"
SETUP_BACKEND: "true"
SETUP_FRONTEND: "false"
volumes:
- "apple_venv:/srv/venv"
- "apple_node_modules:/srv/node_modules"
- ".:/srv/app"
- "../packages:/srv/packages"
- "../tls:/srv/tls"
restart: "no"
stdin_open: true
tty: true
volumes:
apple_venv:
apple_node_modules:

View File

@@ -0,0 +1,16 @@
//
// AppDelegate.h
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <UIKit/UIKit.h>
@class HPAuthFlow;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonnull) HPAuthFlow *authFlow;
@end

View File

@@ -0,0 +1,24 @@
//
// AppDelegate.m
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "AppDelegate.h"
#import "HPAuthFlow.h"
@implementation AppDelegate
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.authFlow = [[HPAuthFlow alloc] init];
return YES;
}
-(UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}
@end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.pl.bthlabs.HotPocket</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocketShared</string>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocket</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLIconFile</key>
<string>icon-mac-384</string>
<key>CFBundleURLName</key>
<string>HotPocketDesktopMac</string>
<key>CFBundleURLSchemes</key>
<array>
<string>hotpocket-mobile</string>
</array>
</dict>
</array>
<key>HPAPIAccessTokenPlatform</key>
<string>iPhone</string>
<key>HPAuthFlowPostAuthenticateURLParts</key>
<dict>
<key>host</key>
<string>post-authenticate</string>
<key>scheme</key>
<string>hotpocket-mobile</string>
</dict>
<key>HPAuthFlowSource</key>
<string>HotPocketMobile</string>
<key>HPRPCClientOrigin</key>
<string>hotpocket-mobile://HPRPCClient</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
</plist>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
//
// SceneDelegate.h
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <UIKit/UIKit.h>
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
@property (strong, nonatomic) UIWindow * window;
@end

View File

@@ -0,0 +1,30 @@
//
// SceneDelegate.m
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "SceneDelegate.h"
#import "AppDelegate.h"
#import "HPAuthFlow.h"
@implementation SceneDelegate
-(void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
for (UIOpenURLContext *context in URLContexts) {
NSURL *url = context.URL;
HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url];
if (receivedAuthParams == nil) {
return;
}
[appDelegate.authFlow handleAuthParams:receivedAuthParams];
}
}
@end

View File

@@ -0,0 +1,18 @@
//
// main.m
// iOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString *appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.Safari.web-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>SafariWebExtensionHandler</string>
</dict>
</dict>
</plist>

View File

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

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>HPAPIAccessTokenPlatform</key>
<string>iPhone</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>ShareExtensionHelper</string>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.pl.bthlabs.HotPocket</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocketShared</string>
<string>$(AppIdentifierPrefix)pl.bthlabs.HotPocket.ShareExtension</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,6 @@
run:
echo: true
pty: true
files_to_version:
- "HotPocket.xcodeproj/project.pbxproj"
- "pyproject.toml"

View File

@@ -0,0 +1,16 @@
//
// AppDelegate.h
// macOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import <Cocoa/Cocoa.h>
@class HPAuthFlow;
@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (strong, nonnull) HPAuthFlow *authFlow;
@end

View File

@@ -0,0 +1,38 @@
//
// AppDelegate.m
// macOS (App)
//
// Created by Tomek Wójcik on 21/08/2025.
//
#import "AppDelegate.h"
#import "HPAuthFlow.h"
#import "HPCredentialsHelper.h"
@implementation AppDelegate
-(void)applicationDidFinishLaunching:(NSNotification *)notification {
self.authFlow = [[HPAuthFlow alloc] init];
}
-(BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
return YES;
}
-(void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
HPAuthParams *receivedAuthParams = nil;
for (NSURL *url in urls) {
receivedAuthParams = [self.authFlow handlePostAuthenticateURL:url];
if (receivedAuthParams != nil) {
break;
}
}
if (receivedAuthParams != nil) {
[self.authFlow handleAuthParams:receivedAuthParams];
}
}
@end

View File

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

View File

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

View File

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

View File

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

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