From 8abc3a8c494e923e8155c2c81c923c2b4834a643 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 23 May 2018 10:23:20 -0400 Subject: [PATCH 1/6] "Bump build to 2.25.1.0." --- Signal/Signal-Info.plist | 4 ++-- SignalShareExtension/Info.plist | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 007d9a951..a02876c86 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.25.0 + 2.25.1 CFBundleSignature ???? CFBundleURLTypes @@ -38,7 +38,7 @@ CFBundleVersion - 2.25.0.28 + 2.25.1.0 ITSAppUsesNonExemptEncryption LOGS_EMAIL diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist index 266bafaff..2bb49b012 100644 --- a/SignalShareExtension/Info.plist +++ b/SignalShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 2.25.0 + 2.25.1 CFBundleVersion - 2.25.0.28 + 2.25.1.0 ITSAppUsesNonExemptEncryption NSAppTransportSecurity From 1343e4bc19d55393b86df90d3b86a260c31a30d0 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 23 May 2018 10:12:16 -0400 Subject: [PATCH 2/6] Preserve legacy outgoing message state; special case contact thread messages. --- .../Utils/MessageRecipientStatusUtils.swift | 2 +- .../Messages/Interactions/TSOutgoingMessage.h | 1 + .../Messages/Interactions/TSOutgoingMessage.m | 48 +++++++++++++++++-- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift b/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift index 9c25b0b25..4d2bc2cd4 100644 --- a/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift +++ b/Signal/src/ViewControllers/Utils/MessageRecipientStatusUtils.swift @@ -129,7 +129,7 @@ public class MessageRecipientStatusUtils: NSObject { if outgoingMessage.readRecipientIds().count > 0 { return (.read, NSLocalizedString("MESSAGE_STATUS_READ", comment: "message footer for read messages")) } - if outgoingMessage.deliveredRecipientIds().count > 0 { + if outgoingMessage.wasDeliveredToAnyRecipient { return (.delivered, NSLocalizedString("MESSAGE_STATUS_DELIVERED", comment: "message status for message delivered to their recipient.")) } diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h index dcb80ebde..953d58e77 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h @@ -109,6 +109,7 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage; @property (readonly) TSOutgoingMessageState messageState; +@property (readonly) BOOL wasDeliveredToAnyRecipient; @property (atomic, readonly) BOOL hasSyncedTranscript; @property (atomic, readonly) NSString *customMessage; diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index b7338735b..839a14e18 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -79,6 +79,10 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt @property (atomic) BOOL isFromLinkedDevice; @property (atomic) TSGroupMetaMessage groupMetaMessage; +@property (nonatomic, readonly) TSOutgoingMessageState legacyMessageState; +@property (nonatomic, readonly) BOOL legacyWasDelivered; +@property (nonatomic, readonly) BOOL hasLegacyMessageState; + @property (atomic, nullable) NSDictionary *recipientStateMap; @end @@ -116,6 +120,8 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt if (messageStateValue) { oldMessageState = (TSOutgoingMessageState)messageStateValue.intValue; } + _hasLegacyMessageState = YES; + _legacyMessageState = oldMessageState; OWSOutgoingMessageRecipientState defaultState; switch (oldMessageState) { @@ -140,6 +146,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt NSArray *_Nullable sentRecipients = [coder decodeObjectForKey:@"sentRecipients"]; NSMutableDictionary *recipientStateMap = [NSMutableDictionary new]; + __block BOOL isGroupThread = NO; // Our default recipient list is the current thread members. __block NSArray *recipientIds = @[]; // To avoid deadlock while migrating these records, we use a dedicated @@ -149,12 +156,25 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt // always accurate, so not using the same connection for both reads is // acceptable. [TSOutgoingMessage.dbMigrationConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - recipientIds = [[self threadWithTransaction:transaction] recipientIdentifiers]; + TSThread *thread = [self threadWithTransaction:transaction]; + recipientIds = [thread recipientIdentifiers]; + isGroupThread = [thread isGroupThread]; }]; - if (sentRecipients) { + + NSNumber *_Nullable wasDelivered = [coder decodeObjectForKey:@"wasDelivered"]; + _legacyWasDelivered = wasDelivered && wasDelivered.boolValue; + BOOL wasDeliveredToContact = NO; + if (isGroupThread) { // If we have a `sentRecipients` list, prefer that as it is more accurate. - recipientIds = sentRecipients; + if (sentRecipients) { + recipientIds = sentRecipients; + } + } else { + // Special-case messages in contact threads; if "was delivered", we know + // it was delivered to the contact. + wasDeliveredToContact = _legacyWasDelivered; } + NSString *_Nullable singleGroupRecipient = [coder decodeObjectForKey:@"singleGroupRecipient"]; if (singleGroupRecipient) { OWSFail(@"%@ unexpected single group recipient message.", self.logTag); @@ -179,6 +199,11 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt // If we have a delivery timestamp for this recipient, mark it as delivered. recipientState.state = OWSOutgoingMessageRecipientStateSent; recipientState.deliveryTimestamp = deliveryTimestamp; + } else if (wasDeliveredToContact) { + OWSAssert(!isGroupThread); + recipientState.state = OWSOutgoingMessageRecipientStateSent; + // Use message time as an estimate of delivery time. + recipientState.deliveryTimestamp = @(self.timestamp); } else if ([sentRecipients containsObject:recipientId]) { // If this recipient is in `sentRecipients`, mark it as sent. recipientState.state = OWSOutgoingMessageRecipientStateSent; @@ -330,7 +355,22 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt - (TSOutgoingMessageState)messageState { - return [TSOutgoingMessage messageStateForRecipientStates:self.recipientStateMap.allValues]; + TSOutgoingMessageState newMessageState = + [TSOutgoingMessage messageStateForRecipientStates:self.recipientStateMap.allValues]; + if (self.hasLegacyMessageState) { + if (newMessageState == TSOutgoingMessageStateSent || self.legacyMessageState == TSOutgoingMessageStateSent) { + return TSOutgoingMessageStateSent; + } + } + return newMessageState; +} + +- (BOOL)wasDeliveredToAnyRecipient +{ + if ([self deliveredRecipientIds].count > 0) { + return YES; + } + return (self.hasLegacyMessageState && self.legacyWasDelivered && self.messageState == TSOutgoingMessageStateSent); } + (TSOutgoingMessageState)messageStateForRecipientStates:(NSArray *)recipientStates From 84776f275719a3eaf87defab33df33f08fa99219 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 23 May 2018 12:37:34 -0400 Subject: [PATCH 3/6] Start timers for migrated messages // FREEBIE --- .../Messages/Interactions/TSOutgoingMessage.m | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index 839a14e18..cc87cc5d3 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -421,16 +421,6 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt [super saveWithTransaction:transaction]; } -- (BOOL)hasSentToAnyRecipient -{ - for (TSOutgoingMessageRecipientState *recipientState in self.recipientStateMap.allValues) { - if (recipientState.state == OWSOutgoingMessageRecipientStateSent) { - return YES; - } - } - return NO; -} - - (BOOL)shouldStartExpireTimerWithTransaction:(YapDatabaseReadTransaction *)transaction { // It's not clear if we should wait until _all_ recipients have reached "sent or later" @@ -442,8 +432,21 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt if (!self.isExpiringMessage) { return NO; + } else if (self.messageState == TSOutgoingMessageStateSent) { + return YES; } else { - return self.hasSentToAnyRecipient; + if (self.expireStartedAt > 0) { + // Our initial migration to populate the recipient state map was incomplete. It's since been + // addressed, but it's possible there are edge cases where a previously sent message would + // no longer be considered sent. + // So here we take extra care not to stop any expiration that had previously started. + // This can also happen under normal cirumstances with an outgoing group message. + DDLogWarn(@"%@ in %s expiration previously started", self.logTag, __PRETTY_FUNCTION__); + + return YES; + } + + return NO; } } From 4e0ce3dbe4be7f01b3bf2d7a4caedcd954d0d646 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 23 May 2018 14:13:53 -0400 Subject: [PATCH 4/6] "Bump build to 2.25.1.1." --- Signal/Signal-Info.plist | 2 +- SignalShareExtension/Info.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index a02876c86..b67ee8730 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -38,7 +38,7 @@ CFBundleVersion - 2.25.1.0 + 2.25.1.1 ITSAppUsesNonExemptEncryption LOGS_EMAIL diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist index 2bb49b012..af0d982f5 100644 --- a/SignalShareExtension/Info.plist +++ b/SignalShareExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 2.25.1 CFBundleVersion - 2.25.1.0 + 2.25.1.1 ITSAppUsesNonExemptEncryption NSAppTransportSecurity From 8cb444088d726ea004a3cb1959b6f0781816df15 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 25 May 2018 09:22:15 -0400 Subject: [PATCH 5/6] "Bump build to 2.25.2.0." --- Signal/Signal-Info.plist | 4 ++-- SignalShareExtension/Info.plist | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index b67ee8730..933d55789 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.25.1 + 2.25.2 CFBundleSignature ???? CFBundleURLTypes @@ -38,7 +38,7 @@ CFBundleVersion - 2.25.1.1 + 2.25.2.0 ITSAppUsesNonExemptEncryption LOGS_EMAIL diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist index af0d982f5..f0ae2d4e1 100644 --- a/SignalShareExtension/Info.plist +++ b/SignalShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 2.25.1 + 2.25.2 CFBundleVersion - 2.25.1.1 + 2.25.2.0 ITSAppUsesNonExemptEncryption NSAppTransportSecurity From 9a34c6804cd6a6c25ee0c2b03c9eeb7dc33e0fe2 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 23 May 2018 17:48:17 -0400 Subject: [PATCH 6/6] policy links // FREEBIE --- .../AppSettings/AboutTableViewController.m | 7 ++ .../Registration/RegistrationViewController.m | 92 +++++++++++++------ .../translations/en.lproj/Localizable.strings | 12 +++ SignalServiceKit/src/TSConstants.h | 3 + 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/Signal/src/ViewControllers/AppSettings/AboutTableViewController.m b/Signal/src/ViewControllers/AppSettings/AboutTableViewController.m index 65614f716..aeeb125b5 100644 --- a/Signal/src/ViewControllers/AppSettings/AboutTableViewController.m +++ b/Signal/src/ViewControllers/AppSettings/AboutTableViewController.m @@ -48,6 +48,13 @@ [informationSection addItem:[OWSTableItem labelItemWithText:NSLocalizedString(@"SETTINGS_VERSION", @"") accessoryText:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]]]; + + [informationSection addItem:[OWSTableItem disclosureItemWithText:NSLocalizedString(@"SETTINGS_LEGAL_TERMS_CELL", + @"table cell label") + actionBlock:^{ + [[UIApplication sharedApplication] + openURL:[NSURL URLWithString:kLegalTermsUrlString]]; + }]]; [contents addSection:informationSection]; OWSTableSection *helpSection = [OWSTableSection new]; diff --git a/Signal/src/ViewControllers/Registration/RegistrationViewController.m b/Signal/src/ViewControllers/Registration/RegistrationViewController.m index 0aae5902d..0f6f9fa40 100644 --- a/Signal/src/ViewControllers/Registration/RegistrationViewController.m +++ b/Signal/src/ViewControllers/Registration/RegistrationViewController.m @@ -70,42 +70,54 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi UIView *headerWrapper = [UIView containerView]; [self.view addSubview:headerWrapper]; headerWrapper.backgroundColor = UIColor.ows_signalBrandBlueColor; - - UIView *headerContent = [UIView new]; - [headerWrapper addSubview:headerContent]; [headerWrapper autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero excludingEdge:ALEdgeBottom]; - [headerContent autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - [headerContent autoPinToTopLayoutGuideOfViewController:self withInset:0]; - [headerContent autoPinWidthToSuperview]; UILabel *headerLabel = [UILabel new]; headerLabel.text = NSLocalizedString(@"REGISTRATION_TITLE_LABEL", @""); headerLabel.textColor = [UIColor whiteColor]; headerLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(20.f, 24.f)]; - [headerContent addSubview:headerLabel]; - [headerLabel autoHCenterInSuperview]; - [headerLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:14.f]; - - CGFloat screenHeight = MAX([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); - if (screenHeight < 568) { - // iPhone 4s or smaller. - [headerContent autoSetDimension:ALDimensionHeight toSize:20]; - headerLabel.hidden = YES; - } else if (screenHeight < 667) { - // iPhone 5 or smaller. - [headerContent autoSetDimension:ALDimensionHeight toSize:80]; - } else { - [headerContent autoSetDimension:ALDimensionHeight toSize:220]; - - UIImage *logo = [UIImage imageNamed:@"logoSignal"]; - OWSAssert(logo); - UIImageView *logoView = [UIImageView new]; - logoView.image = logo; - [headerContent addSubview:logoView]; - [logoView autoHCenterInSuperview]; - [logoView autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:headerLabel withOffset:-14.f]; + + NSString *legalTopMatterFormat = NSLocalizedString(@"REGISTRATION_LEGAL_TOP_MATTER_FORMAT", + @"legal disclaimer, embeds a tappable {{link title}} which is styled as a hyperlink"); + NSString *legalTopMatterLinkWord = NSLocalizedString( + @"REGISTRATION_LEGAL_TOP_MATTER_LINK_TITLE", @"embedded in legal topmatter, styled as a link"); + NSString *legalTopMatter = [NSString stringWithFormat:legalTopMatterFormat, legalTopMatterLinkWord]; + NSMutableAttributedString *attributedLegalTopMatter = + [[NSMutableAttributedString alloc] initWithString:legalTopMatter]; + NSRange linkRange = [legalTopMatter rangeOfString:legalTopMatterLinkWord]; + NSDictionary *linkStyleAttributes = @{ + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid), + }; + [attributedLegalTopMatter setAttributes:linkStyleAttributes range:linkRange]; + + UILabel *legalTopMatterLabel = [UILabel new]; + legalTopMatterLabel.textColor = UIColor.whiteColor; + legalTopMatterLabel.font = UIFont.ows_dynamicTypeFootnoteFont; + legalTopMatterLabel.numberOfLines = 0; + legalTopMatterLabel.textAlignment = NSTextAlignmentCenter; + legalTopMatterLabel.attributedText = attributedLegalTopMatter; + legalTopMatterLabel.userInteractionEnabled = YES; + + UITapGestureRecognizer *tapGesture = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapLegalTerms:)]; + [legalTopMatterLabel addGestureRecognizer:tapGesture]; + + UIStackView *headerContent = [[UIStackView alloc] initWithArrangedSubviews:@[ headerLabel, legalTopMatterLabel ]]; + headerContent.axis = UILayoutConstraintAxisVertical; + headerContent.alignment = UIStackViewAlignmentCenter; + headerContent.spacing = ScaleFromIPhone5To7Plus(8, 16); + headerContent.layoutMarginsRelativeArrangement = YES; + + { + CGFloat topMargin = ScaleFromIPhone5To7Plus(4, 16); + CGFloat bottomMargin = ScaleFromIPhone5To7Plus(8, 16); + headerContent.layoutMargins = UIEdgeInsetsMake(topMargin, 40, bottomMargin, 40); } + [headerWrapper addSubview:headerContent]; + [headerContent autoPinToTopLayoutGuideOfViewController:self withInset:0]; + [headerContent autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeTop]; + const CGFloat kRowHeight = 60.f; const CGFloat kRowHMargin = 20.f; const CGFloat kSeparatorHeight = 1.f; @@ -229,6 +241,25 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi [spinnerView autoSetDimension:ALDimensionHeight toSize:20.f]; [spinnerView autoPinTrailingToSuperviewMarginWithInset:20.f]; [spinnerView stopAnimating]; + + NSString *bottomTermsLinkText = NSLocalizedString(@"REGISTRATION_LEGAL_TERMS_LINK", + @"one line label below submit button on registration screen, which links to an external webpage."); + UIButton *bottomLegalLinkButton = [UIButton new]; + bottomLegalLinkButton.titleLabel.font = UIFont.ows_dynamicTypeFootnoteFont; + [bottomLegalLinkButton setTitleColor:UIColor.ows_materialBlueColor forState:UIControlStateNormal]; + [bottomLegalLinkButton setTitle:bottomTermsLinkText forState:UIControlStateNormal]; + [contentView addSubview:bottomLegalLinkButton]; + [bottomLegalLinkButton addTarget:self + action:@selector(didTapLegalTerms:) + forControlEvents:UIControlEventTouchUpInside]; + + [bottomLegalLinkButton autoPinLeadingAndTrailingToSuperviewMargin]; + [bottomLegalLinkButton autoPinEdge:ALEdgeTop + toEdge:ALEdgeBottom + ofView:activateButton + withOffset:ScaleFromIPhone5To7Plus(8, 12)]; + [bottomLegalLinkButton setCompressionResistanceHigh]; + [bottomLegalLinkButton setContentHuggingHigh]; } - (void)viewDidAppear:(BOOL)animated @@ -350,6 +381,11 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi } } +- (void)didTapLegalTerms:(UIButton *)sender +{ + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:kLegalTermsUrlString]]; +} + - (void)changeCountryCodeTapped { CountryCodeViewController *countryCodeController = [CountryCodeViewController new]; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index a240fbf3a..32f3b84e7 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1600,6 +1600,15 @@ /* alert body during registration */ "REGISTRATION_ERROR_BLANK_VERIFICATION_CODE" = "We can't activate your account until you verify the code we sent you."; +/* one line label below submit button on registration screen, which links to an external webpage. */ +"REGISTRATION_LEGAL_TERMS_LINK" = "Terms & Privacy Policy"; + +/* legal disclaimer, embeds a tappable {{link title}} which is styled as a hyperlink */ +"REGISTRATION_LEGAL_TOP_MATTER_FORMAT" = "By registering this device, you agree to Signal's %@"; + +/* embedded in legal topmatter, styled as a link */ +"REGISTRATION_LEGAL_TOP_MATTER_LINK_TITLE" = "terms"; + /* No comment provided by engineer. */ "REGISTRATION_NON_VALID_NUMBER" = "This phone number format is not supported, please contact support."; @@ -1888,6 +1897,9 @@ /* Label for settings view that allows user to change the notification sound. */ "SETTINGS_ITEM_NOTIFICATION_SOUND" = "Message Sound"; +/* table cell label */ +"SETTINGS_LEGAL_TERMS_CELL" = "Terms & Privacy Policy"; + /* Title for settings activity */ "SETTINGS_NAV_BAR_TITLE" = "Settings"; diff --git a/SignalServiceKit/src/TSConstants.h b/SignalServiceKit/src/TSConstants.h index 3ec11cbce..de19df9c4 100644 --- a/SignalServiceKit/src/TSConstants.h +++ b/SignalServiceKit/src/TSConstants.h @@ -17,6 +17,9 @@ typedef NS_ENUM(NSInteger, TSWhisperMessageType) { #define textSecureHTTPTimeOut 10 +// FIXME this is likely to change +#define kLegalTermsUrlString @"https://signal.org/signal/privacy/" + //#ifndef DEBUG // Production