Skip to content

Commit 13b759b

Browse files
author
Chris Ballinger
committed
Implement decoding of RealtimeRequestBody
Add serializer for RealtimeResponseBody Add Retention Messaging response body
1 parent 6f73956 commit 13b759b

File tree

7 files changed

+412
-0
lines changed

7 files changed

+412
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
2+
3+
from typing import Optional
4+
from attr import define
5+
import attr
6+
7+
@define
8+
class AlternateProduct:
9+
"""
10+
A switch-plan message and product ID you provide in a real-time response to your Get Retention Message endpoint.
11+
12+
https://developer.apple.com/documentation/retentionmessaging/alternateproduct
13+
"""
14+
15+
messageIdentifier: Optional[str] = attr.ib(default=None)
16+
"""
17+
The message identifier of the text to display in the switch-plan retention message.
18+
19+
The message identifier needs to refer to a message that doesn't include an image and that
20+
has a messageState of APPROVED; otherwise, the retention message fails.
21+
22+
https://developer.apple.com/documentation/retentionmessaging/messageidentifier
23+
"""
24+
25+
productId: Optional[str] = attr.ib(default=None)
26+
"""
27+
The product identifier of the subscription the retention message suggests for your customer to switch to.
28+
29+
Use the product identifier in DecodedRealtimeRequestBody to determine the customer's current subscription.
30+
Choose an alternative subscription from the same subscription group.
31+
32+
https://developer.apple.com/documentation/retentionmessaging/productid
33+
"""

appstoreserverlibrary/models/LibraryUtility.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from functools import lru_cache
55
from typing import Any, List, Type, TypeVar
66

7+
import attr
78
from attr import Attribute, has, ib, fields
89
from cattr import override
910
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override
@@ -68,4 +69,23 @@ def _get_cattrs_converter(destination_class: Type[T]) -> cattrs.Converter:
6869
cattrs_overrides[raw_field] = override(rename=matching_name)
6970
c.register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, **cattrs_overrides))
7071
c.register_unstructure_hook_factory(has, lambda cl: make_dict_unstructure_fn(cl, c, **cattrs_overrides))
72+
return c
73+
74+
75+
@lru_cache(maxsize=None)
76+
def _get_retention_response_converter() -> cattrs.Converter:
77+
"""
78+
Special converter for retention messaging responses that omits None/default values.
79+
This is needed for RealtimeResponseBody where fields are mutually exclusive.
80+
"""
81+
c = cattrs.Converter()
82+
83+
# For any attrs class, configure to omit fields with default values
84+
c.register_unstructure_hook_factory(
85+
has,
86+
lambda cls: make_dict_unstructure_fn(
87+
cls, c, _cattrs_omit_if_default=True
88+
)
89+
)
90+
7191
return c
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
2+
3+
from typing import Optional
4+
from attr import define
5+
import attr
6+
7+
@define
8+
class Message:
9+
"""
10+
A message identifier you provide in a real-time response to your Get Retention Message endpoint.
11+
12+
https://developer.apple.com/documentation/retentionmessaging/message
13+
"""
14+
15+
messageIdentifier: Optional[str] = attr.ib(default=None)
16+
"""
17+
The identifier of the message to display to the customer.
18+
19+
The message identifier needs to refer to a message that has a messageState of APPROVED;
20+
otherwise, the retention message fails. If the message includes an image, the image also
21+
needs to have an imageState of APPROVED.
22+
23+
For more information about setting up messages, see Upload Message.
24+
25+
https://developer.apple.com/documentation/retentionmessaging/messageidentifier
26+
"""
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
2+
3+
from typing import Optional
4+
from attr import define
5+
import attr
6+
from .PromotionalOfferSignatureV1 import PromotionalOfferSignatureV1
7+
8+
@define
9+
class PromotionalOffer:
10+
"""
11+
A promotional offer and message you provide in a real-time response to your Get Retention Message endpoint.
12+
13+
https://developer.apple.com/documentation/retentionmessaging/promotionaloffer
14+
"""
15+
16+
messageIdentifier: Optional[str] = attr.ib(default=None)
17+
"""
18+
The identifier of the message to display to the customer, along with the promotional offer.
19+
20+
The message identifier needs to refer to a message that doesn't have an image, and that has
21+
a messageState of APPROVED; otherwise, the retention message fails.
22+
23+
https://developer.apple.com/documentation/retentionmessaging/messageidentifier
24+
"""
25+
26+
promotionalOfferSignatureV2: Optional[str] = attr.ib(default=None)
27+
"""
28+
The promotional offer signature in V2 format. This field is mutually exclusive with
29+
promotionalOfferSignatureV1 field.
30+
31+
For new implementations, consider using this V2 signature, which is easier to generate.
32+
To generate this signature, use the PromotionalOfferV2SignatureCreator class:
33+
34+
Example:
35+
from appstoreserverlibrary.jws_signature_creator import PromotionalOfferV2SignatureCreator
36+
37+
creator = PromotionalOfferV2SignatureCreator(signing_key, key_id, issuer_id, bundle_id)
38+
signature = creator.create_signature(
39+
product_id="com.example.subscription",
40+
offer_identifier="intro_offer",
41+
transaction_id="optional_transaction_id"
42+
)
43+
44+
https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev2
45+
"""
46+
47+
promotionalOfferSignatureV1: Optional[PromotionalOfferSignatureV1] = attr.ib(default=None)
48+
"""
49+
The promotional offer signature in V1 format. This field is mutually exclusive with the
50+
promotionalOfferSignatureV2 field.
51+
52+
To generate this signature, use the PromotionalOfferSignatureCreator class.
53+
See PromotionalOfferSignatureV1 for implementation details.
54+
55+
https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1
56+
"""
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
2+
3+
from typing import Optional
4+
from attr import define
5+
import attr
6+
7+
@define
8+
class PromotionalOfferSignatureV1:
9+
"""
10+
The promotional offer signature you generate using an earlier signature version.
11+
12+
To generate this signature, use the PromotionalOfferSignatureCreator class:
13+
14+
Example:
15+
from appstoreserverlibrary.promotional_offer import PromotionalOfferSignatureCreator
16+
import uuid
17+
import time
18+
19+
creator = PromotionalOfferSignatureCreator(signing_key, key_id, bundle_id)
20+
signature = creator.create_signature(
21+
product_identifier="com.example.subscription",
22+
subscription_offer_id="intro_offer",
23+
application_username="user123",
24+
nonce=uuid.uuid4(),
25+
timestamp=int(time.time() * 1000)
26+
)
27+
28+
https://developer.apple.com/documentation/retentionmessaging/promotionaloffersignaturev1
29+
"""
30+
31+
encodedSignature: Optional[str] = attr.ib(default=None)
32+
"""
33+
The Base64-encoded cryptographic signature you generate using the offer parameters.
34+
35+
https://developer.apple.com/documentation/retentionmessaging/encodedsignature
36+
"""
37+
38+
productId: Optional[str] = attr.ib(default=None)
39+
"""
40+
The subscription's product identifier.
41+
42+
https://developer.apple.com/documentation/retentionmessaging/productid
43+
"""
44+
45+
nonce: Optional[str] = attr.ib(default=None)
46+
"""
47+
A one-time-use UUID antireplay value you generate. Use lowercase.
48+
49+
https://developer.apple.com/documentation/retentionmessaging/nonce
50+
"""
51+
52+
offerId: Optional[str] = attr.ib(default=None)
53+
"""
54+
The promotional offer's identifier.
55+
56+
https://developer.apple.com/documentation/retentionmessaging/offerid
57+
"""
58+
59+
timestamp: Optional[int] = attr.ib(default=None)
60+
"""
61+
The UNIX time, in milliseconds, that you generate the signature.
62+
63+
https://developer.apple.com/documentation/retentionmessaging/timestamp
64+
"""
65+
66+
keyIdentifier: Optional[str] = attr.ib(default=None)
67+
"""
68+
Your private key identifier from App Store Connect.
69+
70+
https://developer.apple.com/documentation/retentionmessaging/keyidentifier
71+
"""
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
2+
3+
from typing import Optional, Dict, Any
4+
from attr import define
5+
import attr
6+
from .Message import Message
7+
from .AlternateProduct import AlternateProduct
8+
from .PromotionalOffer import PromotionalOffer
9+
10+
@define
11+
class RealtimeResponseBody:
12+
"""
13+
A response you provide to choose, in real time, a retention message the system displays to the customer.
14+
15+
Note: The fields in RealtimeResponseBody are mutually exclusive. Choose the type of retention message
16+
to display, and respond using only the field that represents that message type.
17+
18+
https://developer.apple.com/documentation/retentionmessaging/realtimeresponsebody
19+
"""
20+
21+
message: Optional[Message] = attr.ib(default=None)
22+
"""
23+
A retention message that's text-based and can include an optional image.
24+
If you supply this field, don't include the other fields.
25+
26+
https://developer.apple.com/documentation/retentionmessaging/message
27+
"""
28+
29+
alternateProduct: Optional[AlternateProduct] = attr.ib(default=None)
30+
"""
31+
A retention message with a switch-plan option.
32+
If you supply this field, don't include the other fields.
33+
34+
https://developer.apple.com/documentation/retentionmessaging/alternateproduct
35+
"""
36+
37+
promotionalOffer: Optional[PromotionalOffer] = attr.ib(default=None)
38+
"""
39+
A retention message that includes a promotional offer.
40+
If you supply this field, don't include the other fields.
41+
42+
https://developer.apple.com/documentation/retentionmessaging/promotionaloffer
43+
"""
44+
45+
def to_json_dict(self) -> Dict[str, Any]:
46+
"""
47+
Convert to a dictionary suitable for JSON serialization.
48+
Omits None values to maintain mutual exclusivity of fields.
49+
50+
Example:
51+
response = RealtimeResponseBody(
52+
message=Message(messageIdentifier="msg123")
53+
)
54+
json_data = json.dumps(response.to_json_dict())
55+
# Result: {"message": {"messageIdentifier": "msg123"}}
56+
57+
:return: Dictionary representation suitable for JSON encoding
58+
"""
59+
from .LibraryUtility import _get_retention_response_converter
60+
converter = _get_retention_response_converter()
61+
return converter.unstructure(self)

0 commit comments

Comments
 (0)