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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ LIB_SOURCES = Sources/OTifierLib/OTPExtractor.swift \
APP_SOURCES = Sources/OTifierApp/OTifierApp.swift \
Sources/OTifierApp/AppState.swift \
Sources/OTifierApp/OTifierMenu.swift \
Sources/OTifierApp/AccessibilityDragPanel.swift
Sources/OTifierApp/AccessibilityDragPanel.swift \
Sources/OTifierApp/LocalizationManager.swift

LOCALIZATION_CONFIG = Sources/OTifierApp/Localizations.json

BUILD_DIR = .build
APP_BUNDLE = $(BUILD_DIR)/Otifier.app
Expand Down Expand Up @@ -60,7 +63,7 @@ $(BUILD_DIR)/ax-explorer: Sources/ax-explorer/main.swift | $(BUILD_DIR)

app: $(APP_BUNDLE)

$(APP_BUNDLE): check-sparkle $(APP_SOURCES) $(LIB_SOURCES) Sources/OTifierApp/Info.plist AppIcon.icns | $(BUILD_DIR)
$(APP_BUNDLE): check-sparkle $(APP_SOURCES) $(LIB_SOURCES) $(LOCALIZATION_CONFIG) Sources/OTifierApp/Info.plist AppIcon.icns | $(BUILD_DIR)
@echo "Building Otifier.app..."
$(SWIFT) $(SWIFT_FLAGS) \
-F $(SPARKLE_DIR) -framework Sparkle \
Expand All @@ -72,12 +75,13 @@ $(APP_BUNDLE): check-sparkle $(APP_SOURCES) $(LIB_SOURCES) Sources/OTifierApp/In
@cp $(BUILD_DIR)/OTifierApp $(APP_BUNDLE)/Contents/MacOS/Otifier
@cp Sources/OTifierApp/Info.plist $(APP_BUNDLE)/Contents/Info.plist
@cp AppIcon.icns $(APP_BUNDLE)/Contents/Resources/AppIcon.icns
@cp $(LOCALIZATION_CONFIG) $(APP_BUNDLE)/Contents/Resources/Localizations.json
@rm -rf $(APP_BUNDLE)/Contents/Frameworks/Sparkle.framework
@cp -R $(SPARKLE_FRAMEWORK) $(APP_BUNDLE)/Contents/Frameworks/
@codesign --force --sign - --deep $(APP_BUNDLE)
@echo "Built $(APP_BUNDLE)"

release: check-sparkle $(APP_SOURCES) $(LIB_SOURCES) Sources/OTifierApp/Info.plist AppIcon.icns $(ENTITLEMENTS) | $(BUILD_DIR)
release: check-sparkle $(APP_SOURCES) $(LIB_SOURCES) $(LOCALIZATION_CONFIG) Sources/OTifierApp/Info.plist AppIcon.icns $(ENTITLEMENTS) | $(BUILD_DIR)
@if [ -z "$(DEVELOPER_ID)" ]; then \
echo "ERROR: DEVELOPER_ID is not set."; \
echo "Set it via env var or Makefile.local, e.g.:"; \
Expand All @@ -97,6 +101,7 @@ release: check-sparkle $(APP_SOURCES) $(LIB_SOURCES) Sources/OTifierApp/Info.pli
@cp $(BUILD_DIR)/OTifierApp $(APP_BUNDLE)/Contents/MacOS/Otifier
@cp Sources/OTifierApp/Info.plist $(APP_BUNDLE)/Contents/Info.plist
@cp AppIcon.icns $(APP_BUNDLE)/Contents/Resources/AppIcon.icns
@cp $(LOCALIZATION_CONFIG) $(APP_BUNDLE)/Contents/Resources/Localizations.json
@rm -rf $(APP_BUNDLE)/Contents/Frameworks/Sparkle.framework
@cp -R $(SPARKLE_FRAMEWORK) $(APP_BUNDLE)/Contents/Frameworks/
@$(MAKE) sign-sparkle
Expand Down Expand Up @@ -222,7 +227,7 @@ site:
test: $(BUILD_DIR)/test-runner
$(BUILD_DIR)/test-runner

$(BUILD_DIR)/test-runner: Tests/OTifierLibTests/OTPExtractorTests.swift $(LIB_SOURCES) | $(BUILD_DIR)
$(BUILD_DIR)/test-runner: Tests/OTifierLibTests/OTPExtractorTests.swift $(LIB_SOURCES) $(LOCALIZATION_CONFIG) | $(BUILD_DIR)
$(SWIFT) $(SWIFT_FLAGS) -o $@ Tests/OTifierLibTests/OTPExtractorTests.swift $(LIB_SOURCES)

$(BUILD_DIR):
Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ email previews, app push notifications, etc.
- **Just works** — no setup beyond granting Accessibility permission once
- **Universal** — any macOS notification banner, including mirrored iPhone texts
- **History** — recent codes in the menu bar; click any one to re-copy
- **Fallback tools** — choose a code candidate from a recently unrecognized
notification
- **Editable keywords** — review, add, or remove active recognition keywords and
reset the whole list to the project defaults at any time
- **Multilingual menu** — switch between English, Simplified Chinese,
Traditional Chinese, Japanese, Korean, Spanish, French, and German

## Install

Expand Down Expand Up @@ -60,22 +66,26 @@ open .build/Otifier.app

```
iPhone notification → mirrored to Mac → notification banner
→ AX tree poll (1.5s) → verification code match → clipboard
→ AX change event → verification code match → clipboard
```

Otifier polls the Notification Center's Accessibility tree every 1.5 seconds.
When a banner contains a verification code, it copies the code and shows a
small confirmation notification.
Otifier listens for changes to Notification Center's Accessibility tree. A
lightweight window-state check is used as a compatibility fallback. When a
banner contains a verification code, it copies the code and shows a small
confirmation notification.

<details>
<summary>Detection rules</summary>

OTPs are matched via regex with keyword gating to avoid false positives:

- **Patterns**: `code: 123456`, `OTP: 1234`, `G-583920`, bare 4–8 digit codes
- **Keywords**: verification, code, OTP, one-time, 2FA, sign in, 验证码, …
- **Patterns**: `code: 123456`, `OTP: 1234`, `G-583920`, `583 920`, `583-920`
- **Keywords**: verification, code, OTP, one-time, 2FA, sign in, 验证码,
动态口令, 短信码, 安全码, …
- **Filtering**: rejects repeated digits (`1111`), order/tracking numbers,
codes shorter than 4 digits
- **Extensibility**: edit the keyword list from the menu; automatic extraction
remains context-gated, and unmatched candidates can be selected manually

</details>

Expand Down
8 changes: 5 additions & 3 deletions Sources/OTifierApp/AccessibilityDragPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class AccessibilityDragPanelController: NSObject {
private var trackingTimer: Timer?
private var missingWindowSince: Date?

func show() {
func show(message: String) {
if panel != nil {
reposition()
return
Expand All @@ -36,7 +36,7 @@ final class AccessibilityDragPanelController: NSObject {
panel.hasShadow = true
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]

let hosting = NSHostingView(rootView: DragPanelContent())
let hosting = NSHostingView(rootView: DragPanelContent(message: message))
hosting.frame = NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight)
panel.contentView = hosting

Expand Down Expand Up @@ -152,13 +152,15 @@ final class AccessibilityDragPanelController: NSObject {
private let arrowBlue = Color(red: 0x54 / 255.0, green: 0xB6 / 255.0, blue: 0xFF / 255.0)

private struct DragPanelContent: View {
let message: String

var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Image(systemName: "arrow.up")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(arrowBlue)
Text("Drag Otifier to the list above to allow Accessibility")
Text(message)
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.primary)
Spacer(minLength: 0)
Expand Down
Loading