feat(ios): share extension ile Ratebubble iOS istemcisini ekle ve paylaşım akışını düzelt

This commit is contained in:
2026-03-01 18:07:07 +03:00
parent 8c66fa9b82
commit 5c6a829a4d
20 changed files with 1235 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
---
date: 2026-03-01
topic: ios-share-extension-v1
---
# iOS Share Extension v1
## What We're Building
Projeye ikinci bir frontend olarak native iOS uygulaması eklenecek. Uygulamanın v1 ana işlevi, Netflix uygulamasından paylaşılan içerik linkini almak ve backend API'ye göndererek metadata sonucunu kullanıcıya göstermek.
Akış: Netflix içerik sayfası -> Paylaş -> bizim iOS app (Share Extension) -> URL alma -> backend `/api/getinfo` isteği -> sonuçları metin olarak gösterme (`title`, `year`, vb.).
## Why This Approach
Share Extension seçimi, iOS paylaşım menüsüne doğal şekilde entegre olur ve kullanıcı davranışıyla birebir örtüşür. Deep link tabanlı alternatiflere göre daha güvenilir URL yakalama sağlar ve v1 için en düşük sürtünmeyle çalışır.
V1 kapsamını sadece “link al, API çağır, sonucu göster” ile sınırlamak, YAGNI prensibine uygundur ve ürünü hızlıca canlı doğrulamaya taşır.
## Key Decisions
- Entegrasyon tipi: Share Extension (zorunlu).
- Kapsam: Sadece Netflix paylaşım linki ile `/api/getinfo` çağrısı.
- Yetkilendirme: `X-API-Key` olarak `API_KEY_MOBILE` kullanılacak.
- Görsellik: UI/UX tasarımı v1 sonrası iterasyona bırakılacak.
## Open Questions
- Share Extension URLyi doğrudan APIye mi gönderecek, yoksa ana appe handoff edip ana app mi çağrı yapacak?
- Başarısız API yanıtlarında kullanıcıya minimum hangi hata metni gösterilecek?
## Next Steps
-> `/workflows:plan`

39
ios/README.md Normal file
View File

@@ -0,0 +1,39 @@
# Ratebubble iOS (v1)
Bu klasör, Ratebubble için iOS ana uygulama + Share Extension iskeletini içerir.
## Hedef
- Netflix uygulamasından paylaşılan URL'yi almak
- Ana app'e handoff etmek
- Backend `/api/getinfo` endpointine gönderip sonucu göstermek
## Gereksinimler
- Xcode 15+
- iOS 16+
- (Opsiyonel) XcodeGen
## Proje Oluşturma (XcodeGen)
```bash
cd ios
xcodegen generate
open Ratebubble.xcodeproj
```
Eğer `xcodegen` kurulu değilse:
```bash
brew install xcodegen
```
## Yapılandırma
`Ratebubble/Resources/Config.xcconfig` dosyasında:
- `API_BASE_URL`
- `MOBILE_API_KEY`
- `APP_GROUP_ID`
- `APP_URL_SCHEME`
değerlerini ortamına göre güncelle.
## Not
Share Extension, URL'yi App Group `UserDefaults` içine yazar ve custom URL scheme ile ana app'i açar.

View File

@@ -0,0 +1,454 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 63;
objects = {
/* Begin PBXBuildFile section */
0315885AA91662FE48BBC594 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E2C9211EC8E30B83ABBCFB /* ContentView.swift */; };
0C602ACFD0DC100ECCFA84A5 /* SharedPayloadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACEBEAEDF343884A3712AB1 /* SharedPayloadStore.swift */; };
17157E8CF084270023A56C06 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA40D68C79904DEE4125D874 /* Models.swift */; };
186EA66540EFA4CEA949617D /* RatebubbleShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7539736C403588176FB7D80C /* RatebubbleShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
3993F1B14739F8177B844EE0 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DD3EAEF55CD1A6EF04D923 /* APIClient.swift */; };
3E1D36E3AE2CDBD9C402E921 /* RatebubbleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC8FF0D6783D4BFC590D370 /* RatebubbleApp.swift */; };
77D601458525635F705EA471 /* SharedPayloadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CACEBEAEDF343884A3712AB1 /* SharedPayloadStore.swift */; };
886AF155C91E366210524E45 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37749607968C1A751317B7BD /* MainViewModel.swift */; };
A547A283AD74B06B25FDB424 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C1C571628107632B4A2F90 /* ShareViewController.swift */; };
B31B1B7EC5BAE4FB9011C94C /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3DD3EAEF55CD1A6EF04D923 /* APIClient.swift */; };
F670AE07545EBB4ABE2889F7 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA40D68C79904DEE4125D874 /* Models.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
2FFA444A9840D8FEE9B59CB3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 7944D79A93FF32A326D78AB8 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D66EABC12ABB12431BD3554C;
remoteInfo = RatebubbleShare;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
87D7EB893873C1953A4B8483 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
186EA66540EFA4CEA949617D /* RatebubbleShare.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
19F8E500E3A70D57454E49A6 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
37749607968C1A751317B7BD /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = "<group>"; };
72C1C571628107632B4A2F90 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
7539736C403588176FB7D80C /* RatebubbleShare.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RatebubbleShare.appex; sourceTree = BUILT_PRODUCTS_DIR; };
7FEF3C0785A60EA1B560627C /* Ratebubble.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ratebubble.app; sourceTree = BUILT_PRODUCTS_DIR; };
96E2C9211EC8E30B83ABBCFB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
AA40D68C79904DEE4125D874 /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = "<group>"; };
CACEBEAEDF343884A3712AB1 /* SharedPayloadStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedPayloadStore.swift; sourceTree = "<group>"; };
D3DD3EAEF55CD1A6EF04D923 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = "<group>"; };
FEC8FF0D6783D4BFC590D370 /* RatebubbleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatebubbleApp.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
0EF0C634DF5D5383BE2D7771 /* Shared */ = {
isa = PBXGroup;
children = (
D3DD3EAEF55CD1A6EF04D923 /* APIClient.swift */,
AA40D68C79904DEE4125D874 /* Models.swift */,
CACEBEAEDF343884A3712AB1 /* SharedPayloadStore.swift */,
);
name = Shared;
path = Ratebubble/Shared;
sourceTree = "<group>";
};
108289137F406D4F63BFC3AE /* ShareExtension */ = {
isa = PBXGroup;
children = (
72C1C571628107632B4A2F90 /* ShareViewController.swift */,
);
name = ShareExtension;
path = Ratebubble/ShareExtension;
sourceTree = "<group>";
};
1C0E0078471B0855017A64DD = {
isa = PBXGroup;
children = (
EF496B275DF9294D162CFA6E /* App */,
BFD8EEAEAAE9601A16238D2D /* Resources */,
0EF0C634DF5D5383BE2D7771 /* Shared */,
108289137F406D4F63BFC3AE /* ShareExtension */,
E032C44ACA0299B26854540A /* Products */,
);
sourceTree = "<group>";
};
BFD8EEAEAAE9601A16238D2D /* Resources */ = {
isa = PBXGroup;
children = (
19F8E500E3A70D57454E49A6 /* Config.xcconfig */,
);
name = Resources;
path = Ratebubble/Resources;
sourceTree = "<group>";
};
E032C44ACA0299B26854540A /* Products */ = {
isa = PBXGroup;
children = (
7FEF3C0785A60EA1B560627C /* Ratebubble.app */,
7539736C403588176FB7D80C /* RatebubbleShare.appex */,
);
name = Products;
sourceTree = "<group>";
};
EF496B275DF9294D162CFA6E /* App */ = {
isa = PBXGroup;
children = (
96E2C9211EC8E30B83ABBCFB /* ContentView.swift */,
37749607968C1A751317B7BD /* MainViewModel.swift */,
FEC8FF0D6783D4BFC590D370 /* RatebubbleApp.swift */,
);
name = App;
path = Ratebubble/App;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
1E5B7142527B8E64D5BB475A /* Ratebubble */ = {
isa = PBXNativeTarget;
buildConfigurationList = DE6F9D82ED742462C54ED81A /* Build configuration list for PBXNativeTarget "Ratebubble" */;
buildPhases = (
21317B3B7EEF00F8D3448F1A /* Sources */,
87D7EB893873C1953A4B8483 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
B360B60BC3BB55F1DEFD5F02 /* PBXTargetDependency */,
);
name = Ratebubble;
packageProductDependencies = (
);
productName = Ratebubble;
productReference = 7FEF3C0785A60EA1B560627C /* Ratebubble.app */;
productType = "com.apple.product-type.application";
};
D66EABC12ABB12431BD3554C /* RatebubbleShare */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3BCC0C2B27341BF3ADDCB3B9 /* Build configuration list for PBXNativeTarget "RatebubbleShare" */;
buildPhases = (
B64AC19652E4EA372111002C /* Sources */,
);
buildRules = (
);
dependencies = (
);
name = RatebubbleShare;
packageProductDependencies = (
);
productName = RatebubbleShare;
productReference = 7539736C403588176FB7D80C /* RatebubbleShare.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
7944D79A93FF32A326D78AB8 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
};
buildConfigurationList = BA6BB0478ED529686B10D5F3 /* Build configuration list for PBXProject "Ratebubble" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = 1C0E0078471B0855017A64DD;
minimizedProjectReferenceProxies = 1;
projectDirPath = "";
projectRoot = "";
targets = (
1E5B7142527B8E64D5BB475A /* Ratebubble */,
D66EABC12ABB12431BD3554C /* RatebubbleShare */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
21317B3B7EEF00F8D3448F1A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B31B1B7EC5BAE4FB9011C94C /* APIClient.swift in Sources */,
0315885AA91662FE48BBC594 /* ContentView.swift in Sources */,
886AF155C91E366210524E45 /* MainViewModel.swift in Sources */,
17157E8CF084270023A56C06 /* Models.swift in Sources */,
3E1D36E3AE2CDBD9C402E921 /* RatebubbleApp.swift in Sources */,
77D601458525635F705EA471 /* SharedPayloadStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B64AC19652E4EA372111002C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3993F1B14739F8177B844EE0 /* APIClient.swift in Sources */,
F670AE07545EBB4ABE2889F7 /* Models.swift in Sources */,
A547A283AD74B06B25FDB424 /* ShareViewController.swift in Sources */,
0C602ACFD0DC100ECCFA84A5 /* SharedPayloadStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
B360B60BC3BB55F1DEFD5F02 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D66EABC12ABB12431BD3554C /* RatebubbleShare */;
targetProxy = 2FFA444A9840D8FEE9B59CB3 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
03B172804432CFDEF0B15A28 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 19F8E500E3A70D57454E49A6 /* Config.xcconfig */;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = Ratebubble/Resources/RatebubbleShare.entitlements;
DEVELOPMENT_TEAM = S34SFUY9SC;
INFOPLIST_FILE = "Ratebubble/Resources/RatebubbleShare-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = net.wisecolt.ratebubble.share;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
2C4CEF1731FBB13FC87F825C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.wisecolt.ratebubble;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.9;
};
name = Release;
};
3AE70298876BFAD681CF451B /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 19F8E500E3A70D57454E49A6 /* Config.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Ratebubble/Resources/Ratebubble.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
DEVELOPMENT_TEAM = S34SFUY9SC;
INFOPLIST_FILE = "Ratebubble/Resources/Ratebubble-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
3B575860868E0B1AF5B7D410 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.wisecolt.ratebubble;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.9;
};
name = Debug;
};
5C1878476CD3B8F15D53C417 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 19F8E500E3A70D57454E49A6 /* Config.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Ratebubble/Resources/Ratebubble.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
DEVELOPMENT_TEAM = S34SFUY9SC;
INFOPLIST_FILE = "Ratebubble/Resources/Ratebubble-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
A005F6D39148A83B59423F17 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 19F8E500E3A70D57454E49A6 /* Config.xcconfig */;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = Ratebubble/Resources/RatebubbleShare.entitlements;
DEVELOPMENT_TEAM = S34SFUY9SC;
INFOPLIST_FILE = "Ratebubble/Resources/RatebubbleShare-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = net.wisecolt.ratebubble.share;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
3BCC0C2B27341BF3ADDCB3B9 /* Build configuration list for PBXNativeTarget "RatebubbleShare" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A005F6D39148A83B59423F17 /* Debug */,
03B172804432CFDEF0B15A28 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
BA6BB0478ED529686B10D5F3 /* Build configuration list for PBXProject "Ratebubble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3B575860868E0B1AF5B7D410 /* Debug */,
2C4CEF1731FBB13FC87F825C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
DE6F9D82ED742462C54ED81A /* Build configuration list for PBXNativeTarget "Ratebubble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3AE70298876BFAD681CF451B /* Debug */,
5C1878476CD3B8F15D53C417 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = 7944D79A93FF32A326D78AB8 /* Project object */;
}

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 = "2620"
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 = "1E5B7142527B8E64D5BB475A"
BuildableName = "Ratebubble.app"
BlueprintName = "Ratebubble"
ReferencedContainer = "container:Ratebubble.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 = "1E5B7142527B8E64D5BB475A"
BuildableName = "Ratebubble.app"
BlueprintName = "Ratebubble"
ReferencedContainer = "container:Ratebubble.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1E5B7142527B8E64D5BB475A"
BuildableName = "Ratebubble.app"
BlueprintName = "Ratebubble"
ReferencedContainer = "container:Ratebubble.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 = "2620"
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 = "D66EABC12ABB12431BD3554C"
BuildableName = "RatebubbleShare.appex"
BlueprintName = "RatebubbleShare"
ReferencedContainer = "container:Ratebubble.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "1E5B7142527B8E64D5BB475A"
BuildableName = "Ratebubble.app"
BlueprintName = "Ratebubble"
ReferencedContainer = "container:Ratebubble.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 = "1E5B7142527B8E64D5BB475A"
BuildableName = "Ratebubble.app"
BlueprintName = "Ratebubble"
ReferencedContainer = "container:Ratebubble.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 = "1E5B7142527B8E64D5BB475A"
BuildableName = "Ratebubble.app"
BlueprintName = "Ratebubble"
ReferencedContainer = "container:Ratebubble.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,82 @@
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = MainViewModel()
@Environment(\.scenePhase) private var scenePhase
var body: some View {
NavigationStack {
Form {
Section("Paylaşılan Link") {
TextField("https://www.netflix.com/tr/title/...", text: $viewModel.sharedURL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled(true)
.keyboardType(.URL)
Button("Backend'den Getir") {
Task { await viewModel.fetch() }
}
.disabled(viewModel.isLoading)
}
if viewModel.isLoading {
Section {
HStack {
ProgressView()
Text("Veri alınıyor...")
}
}
}
if let error = viewModel.errorMessage {
Section("Hata") {
Text(error)
.foregroundStyle(.red)
}
}
if let result = viewModel.result {
Section("Sonuç") {
KeyValueRow(key: "Provider", value: result.provider)
KeyValueRow(key: "Title", value: result.title)
KeyValueRow(key: "Year", value: result.year.map(String.init) ?? "-")
KeyValueRow(key: "Type", value: result.type)
KeyValueRow(key: "Age Rating", value: result.ageRating ?? "-")
KeyValueRow(key: "Current Season", value: result.currentSeason.map(String.init) ?? "-")
KeyValueRow(key: "Genres", value: result.genres.joined(separator: ", "))
KeyValueRow(key: "Cast", value: result.cast.joined(separator: ", "))
KeyValueRow(key: "Plot", value: result.plot ?? "-")
}
}
}
.navigationTitle("Ratebubble")
}
.onAppear {
viewModel.consumeSharedURLIfAny()
}
.onOpenURL { _ in
viewModel.consumeSharedURLIfAny()
}
.onChange(of: scenePhase) { phase in
if phase == .active {
viewModel.consumeSharedURLIfAny()
}
}
}
}
private struct KeyValueRow: View {
let key: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(key)
.font(.caption)
.foregroundStyle(.secondary)
Text(value.isEmpty ? "-" : value)
.font(.body)
}
.padding(.vertical, 2)
}
}

View File

@@ -0,0 +1,36 @@
import Foundation
@MainActor
final class MainViewModel: ObservableObject {
@Published var sharedURL: String = ""
@Published var isLoading: Bool = false
@Published var result: GetInfoResponse?
@Published var errorMessage: String?
func consumeSharedURLIfAny() {
guard let incoming = SharedPayloadStore.consumeIncomingURL(), !incoming.isEmpty else {
return
}
sharedURL = incoming
Task { await fetch() }
}
func fetch() async {
guard !sharedURL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
errorMessage = "Paylaşılan URL boş olamaz."
return
}
isLoading = true
errorMessage = nil
result = nil
do {
result = try await APIClient.shared.getInfo(url: sharedURL)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}

View File

@@ -0,0 +1,10 @@
import SwiftUI
@main
struct RatebubbleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

View File

@@ -0,0 +1,5 @@
SLASH = /
API_BASE_URL = http:$(SLASH)$(SLASH)localhost:3000
MOBILE_API_KEY = mobile-dev-key-change-me
APP_GROUP_ID = group.net.wisecolt.ratebubble
APP_URL_SCHEME = ratebubble

View File

@@ -0,0 +1,39 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>$(APP_URL_SCHEME)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>API_BASE_URL</key>
<string>$(API_BASE_URL)</string>
<key>MOBILE_API_KEY</key>
<string>$(MOBILE_API_KEY)</string>
<key>UILaunchScreen</key>
<dict/>
</dict>
</plist>

View File

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

View File

@@ -0,0 +1,36 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleDisplayName</key>
<string>Ratebubble Share</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<string>TRUEPREDICATE</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
</dict>
</dict>
</plist>

View File

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

View File

@@ -0,0 +1,107 @@
import UIKit
import UniformTypeIdentifiers
final class ShareViewController: UIViewController {
private let statusLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
Task { await handleIncomingShare() }
}
private func setupUI() {
view.backgroundColor = .systemBackground
statusLabel.translatesAutoresizingMaskIntoConstraints = false
statusLabel.textAlignment = .center
statusLabel.numberOfLines = 0
statusLabel.text = "Paylaşılan bağlantı alınıyor..."
view.addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
@MainActor
private func updateStatus(_ text: String) {
statusLabel.text = text
}
private func handleIncomingShare() async {
guard let item = extensionContext?.inputItems.first as? NSExtensionItem,
let providers = item.attachments else {
updateStatus("Paylaşılan içerik okunamadı.")
return
}
for provider in providers {
if let extracted = await extractURL(from: provider), isSupportedStreamingURL(extracted) {
SharedPayloadStore.saveIncomingURL(extracted.absoluteString)
updateStatus("Bağlantı alındı, uygulama açılıyor...")
openHostApp()
return
}
}
updateStatus("Geçerli bir Netflix/Prime Video linki bulunamadı.")
}
private func extractURL(from provider: NSItemProvider) async -> URL? {
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
return await withCheckedContinuation { continuation in
provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { item, _ in
continuation.resume(returning: item as? URL)
}
}
}
if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) {
return await withCheckedContinuation { continuation in
provider.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { item, _ in
if let raw = item as? String, let url = URL(string: raw.trimmingCharacters(in: .whitespacesAndNewlines)) {
continuation.resume(returning: url)
return
}
continuation.resume(returning: nil)
}
}
}
return nil
}
private func isSupportedStreamingURL(_ url: URL) -> Bool {
let host = url.host?.lowercased() ?? ""
let netflixHosts = ["www.netflix.com", "netflix.com", "www.netflix.com.tr", "netflix.com.tr"]
let primeHosts = ["www.primevideo.com", "primevideo.com", "www.amazon.com", "amazon.com"]
let isNetflix = netflixHosts.contains(host)
let isPrime = primeHosts.contains(host)
guard isNetflix || isPrime else { return false }
let path = url.path.lowercased()
if path.contains("/title/") || path.contains("/watch/") || path.contains("/detail/") {
return true
}
// Some share links can be shortened/redirect style without a canonical path.
return !path.isEmpty && path != "/"
}
private func openHostApp() {
guard let url = URL(string: "\(SharedConfig.appURLScheme)://ingest") else {
extensionContext?.completeRequest(returningItems: nil)
return
}
extensionContext?.open(url) { success in
// If opening succeeded, the system should transition to the host app.
// Completing the extension request immediately can bounce back to the source app.
guard !success else { return }
self.extensionContext?.completeRequest(returningItems: nil)
}
}
}

View File

@@ -0,0 +1,65 @@
import Foundation
enum APIClientError: LocalizedError {
case invalidBaseURL
case invalidResponse
case server(String)
var errorDescription: String? {
switch self {
case .invalidBaseURL:
return "API_BASE_URL geçersiz."
case .invalidResponse:
return "Sunucudan geçerli yanıt alınamadı."
case .server(let message):
return message
}
}
}
final class APIClient {
static let shared = APIClient()
private init() {}
private var baseURL: URL? {
guard let raw = Bundle.main.object(forInfoDictionaryKey: "API_BASE_URL") as? String else {
return nil
}
return URL(string: raw)
}
private var mobileAPIKey: String {
Bundle.main.object(forInfoDictionaryKey: "MOBILE_API_KEY") as? String
?? "mobile-dev-key-change-me"
}
func getInfo(url: String) async throws -> GetInfoResponse {
guard let baseURL else { throw APIClientError.invalidBaseURL }
var request = URLRequest(url: baseURL.appending(path: "/api/getinfo"))
request.httpMethod = "POST"
request.timeoutInterval = 20
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(mobileAPIKey, forHTTPHeaderField: "X-API-Key")
request.httpBody = try JSONEncoder().encode(GetInfoRequest(url: url))
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw APIClientError.invalidResponse
}
let decoder = JSONDecoder()
let envelope = try decoder.decode(APIEnvelope<GetInfoResponse>.self, from: data)
if (200..<300).contains(http.statusCode), envelope.success, let payload = envelope.data {
return payload
}
if let errorMessage = envelope.error?.message {
throw APIClientError.server(errorMessage)
}
throw APIClientError.server("İstek başarısız oldu (\(http.statusCode)).")
}
}

View File

@@ -0,0 +1,29 @@
import Foundation
struct GetInfoRequest: Encodable {
let url: String
}
struct APIErrorPayload: Decodable, Error {
let code: String
let message: String
}
struct APIEnvelope<T: Decodable>: Decodable {
let success: Bool
let data: T?
let error: APIErrorPayload?
}
struct GetInfoResponse: Decodable {
let provider: String
let title: String
let year: Int?
let plot: String?
let ageRating: String?
let type: String
let genres: [String]
let cast: [String]
let backdrop: String?
let currentSeason: Int?
}

View File

@@ -0,0 +1,31 @@
import Foundation
enum SharedConfig {
static var appGroupID: String {
Bundle.main.object(forInfoDictionaryKey: "APP_GROUP_ID") as? String
?? "group.net.wisecolt.ratebubble"
}
static var appURLScheme: String {
Bundle.main.object(forInfoDictionaryKey: "APP_URL_SCHEME") as? String
?? "ratebubble"
}
}
enum SharedKeys {
static let incomingURL = "incoming_shared_url"
}
enum SharedPayloadStore {
static func saveIncomingURL(_ url: String) {
guard let defaults = UserDefaults(suiteName: SharedConfig.appGroupID) else { return }
defaults.set(url, forKey: SharedKeys.incomingURL)
defaults.synchronize()
}
static func consumeIncomingURL() -> String? {
guard let defaults = UserDefaults(suiteName: SharedConfig.appGroupID) else { return nil }
defer { defaults.removeObject(forKey: SharedKeys.incomingURL) }
return defaults.string(forKey: SharedKeys.incomingURL)
}
}

58
ios/project.yml Normal file
View File

@@ -0,0 +1,58 @@
name: Ratebubble
options:
deploymentTarget:
iOS: 16.0
configs:
Debug: debug
Release: release
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: net.wisecolt.ratebubble
SWIFT_VERSION: 5.9
targets:
Ratebubble:
type: application
platform: iOS
deploymentTarget: "16.0"
sources:
- path: Ratebubble/App
- path: Ratebubble/Shared
configFiles:
Debug: Ratebubble/Resources/Config.xcconfig
Release: Ratebubble/Resources/Config.xcconfig
info:
path: Ratebubble/Resources/Ratebubble-Info.plist
properties:
UILaunchScreen: {}
CFBundleURLTypes:
- CFBundleTypeRole: Editor
CFBundleURLSchemes:
- $(APP_URL_SCHEME)
entitlements:
path: Ratebubble/Resources/Ratebubble.entitlements
properties:
com.apple.security.application-groups:
- $(APP_GROUP_ID)
dependencies:
- target: RatebubbleShare
RatebubbleShare:
type: app-extension
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: net.wisecolt.ratebubble.share
platform: iOS
deploymentTarget: "16.0"
sources:
- path: Ratebubble/ShareExtension
- path: Ratebubble/Shared
configFiles:
Debug: Ratebubble/Resources/Config.xcconfig
Release: Ratebubble/Resources/Config.xcconfig
info:
path: Ratebubble/Resources/RatebubbleShare-Info.plist
entitlements:
path: Ratebubble/Resources/RatebubbleShare.entitlements
properties:
com.apple.security.application-groups:
- $(APP_GROUP_ID)

13
ios/scripts/bootstrap.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
if ! command -v xcodegen >/dev/null 2>&1; then
echo "xcodegen bulunamadı. Kurulum: brew install xcodegen"
exit 1
fi
xcodegen generate
echo "Tamamlandı: ios/Ratebubble.xcodeproj oluşturuldu."